Amazon Dash Buttonの電池交換に挑戦して失敗した。

やっちゃった~~~。失敗しました。

Amazon Dash Buttonを2つ持っているのですが、片方のバッテリー残量が10%を切っていたので電池を交換することにしました。
色々参考になるページがあります。ありがたいです。
Amazon Dash Buttonの電池が切れたので交換した - にるぽっぽ
アマゾンダッシュボタンを分解する。 - まず分解。

マイナスドライバで穴をあけて、そこから梃子でフタを剥がしました。

何気ないマイナスドライバが基板を傷つけた。

何気ないマイナスドライバが基板を傷つけた

あけた穴が基板を貫通していました。愚かですね。フタの境目に穴を開けるのではなく、底の方にカッターで切れ目を入れてマイナスドライバを差し込めばこんなことにはならなかったです。
モニタの入力端子切り替えに使っているボタンだったのですが、無いとめちゃくちゃ不便なので、AWS IoTボタンより手頃な値段の代替機がほしいところです。

いくつか条件がありますが、Amazon Dash Buttonはサービス停止後もまだ使える代物なので、復旧させてみたい方は以下の記事を読んでみてください。
Wi-Fiに繋がらないAmazon Dash Buttonを復旧させて出勤ダッシュボタンを作る。 - Qiita

ARRISのケーブルモデムの脆弱性(2015年発見)を2020年に知った話。

今NURO光がアツイですね。
NURO光はセキュリティ的にやばいって話 (安全に使うための方法) - Qiita
NURO光、今の家に引っ越してきたときに契約を検討してたのですが、工事の前段階になって業者と一切連絡取れなくなって、結局そのまま契約できませんでした。
光回線が開通するまで仮でCATVのインターネットサービスを契約したのですが、支障なく使用できるレベルだったので、未だにCATVを使い続けています。

ARRISという会社のケーブルモデムがレンタルされまして、その下に自分たちで用意したルータ接続して使っています。そういえばケーブルモデムのセキュリティってどうなってんのかなぁと思って調べてみたら、なかなかすごいのがみつかりました。
ARRIS Cable Modem Has a Backdoor in the Backdoor | Hacker News
w00tsec: ARRIS Cable Modem has a Backdoor in the Backdoor

ARRIS password of the day is a remote backdoor known since 2009. It uses a DES encoded seed (set by the ISP using the arrisCmDoc30AccessClientSeed MIB) to generate a daily backdoor password. The default seed is MPSJKMDHAI and guess what – many ISPs won’t bother changing it at all.

いうてもうシード変更してくれてるでしょと思ったら変更されてなかったし、普通にコンソールに入れてめちゃくちゃ笑いました。やべえ。ファームウェアのバージョンも最新じゃないし(多分デフォルト)対応してほしいのでCATVに問い合わせました。NURO光馬鹿にできなかった。
一応グローバルIPからコンパネに入れないことは確認したけど不安ですね…。ちなみにCATV契約したのは2015年をとっくに越えたあとだったので、なんでこんなことになってるかは知らないです。

2020/06/02 追記

CATVの会社に問い合わせた結果が返ってきました。
問題は把握済みで、外部からのルートは塞いであるのと、ファームウェアはあえてバージョンを上げていないとのことでした。
外からいじられて困る項目は全部無効化しといたのでとりあえずは大丈夫かな~と思っています。

ポケモンホームのバトルデータをPCのブラウザで表示する。

ポケモンホームという最高にUIと機能が終わってるアプリがあるのですが、ついにランクバトルとインターネット大会の統計情報が見られるようになりました。
バトルデータの内容はWebViewで表示されています。
ただUIが終わっていて癖があって死ぬほど操作しづらいのと、バグっててページ送りができないため、素のブラウザで情報を見ることにします。

どちらもログインなしで参照できます。

  • ランクバトル
    • https://resource.pokemon-home.com/battledata/rankmatch_detail.html
  • これまでに開催された大会
    • https://resource.pokemon-home.com/battledata/internetcompetition_list.html

タイトルがRankumatchなんですが大丈夫なんでしょうか。ほんと心配になる。

認証系も突破できそうなのですが、詳細書くのも良くないのでここでは言及しません。

URLの調査にFiddlerを使いました。やっぱり便利ですね。
早くブラウザ版を公開してほしいのと、このレベルでお金とるのマジでやめてほしいな~って思いました。
名探偵ピカチュウの地上波放送に剣盾のCM挟むのほんとずるいですね。

WebARENA IndigoがCentOS 8.1対応したので乗り換えた。

「Indigo」16GB・32GBメモリのインスタンスとCentOS8.1の提供開始 | 公式 WebARENA(ウェブアリーナ) | レンタルサーバー,VPS,クラウド,メールサーバー, 専用サーバー,データセンター
こんな状況だし絶対対応しないだろうと思ってたら提供開始されたので、retrorocket.bizで使ってるサーバも乗り換えました。ありがてぇ~。

移行した感想ですが、CentOS 6->7のときほどひどくはないものの、それでも挙動がクソすぎて5回ぐらいキレてたので、ただでさえストレスがたまりがちな社会状況下で好き好んでやる作業じゃねえなという気持ちです。ほんとはこの時間で炊飯器パン作りに挑戦したかったのですが、ドライイーストがどこにも売ってませんでした。人間考えることって同じなんですね。

CentOS 7と挙動が違うorバグっぽいところ

umaskの値が0022から0077になっている(要検証)

CentOS 7まで、ログインシェルがbash以外でも0022を指定してくれたのですが、今回は明示的に指定がないと0077になるみたいです。CentOS 8が悪いのかIndigoの提供してるサーバの独自設定なのかfishの問題かは調べないとわからん。(私の使ってるログインシェルはfishなのですが、zshとかなら大丈夫かもしれないですね。)
この挙動で最もやばい部分は、共有ライブラリのディレクトリに書き込みが発生するコマンド(cpanm等)をsudoで実行すると、作成されたディレクトリのパーミッションが軒並み「所有者rootの700」になることです。
おかげさまでPerlで書いてあるスクリプトが、軒並みライブラリが読めずに動作しなくなりました。perldocすら動かなくなったときは流石に焦った。
bash以外を使ってる方で、何かしらスクリプトを実行した時に「権限がありません」と怒られるなら、この挙動を疑ってみるといいと思います。

dnf listコマンドでconflicting requestsエラーが出る

CentOS 8 conflicting requests
上記記事と同様にnothing provides module(perl:5.26) needed by module perl-DBIが出たので、dnf module enable perl:5.26して対処しました。

dnf installでいつまでもパッケージのダウンロードが始まらない(要検証)

バグっぽいのですが、同じ事象を報告してる人が見つからないのでIndigo固有の問題かもしれない。一度キャンセルして再実行すると直ります。

個人的にふざけんなって思ったところ

firewalldがデフォルトでcockpitのポートを許可している

firewall-cmd --list-all
(略)
services: cockpit dhcpv6-client ssh

おいやめろ!!!!!!!!!!!
今後cockpit狙った攻撃が増えるんだろうなと思うとワクワクしますね。
cockpit、今回は速攻でremoveしましたが、触ってみたいのでインターネットに出てないマシンで試そうと思います。
removeしたくないけどActivate the web console with: systemctl enable --now cockpit.socketの表示が嫌な方は、/etc/motd.d/cockpitを削除するといいです。

感想

趣味で使ってるサーバなら乗り換えてもいいけど、もし業務で使うならもう少し情報が出揃うまで待ってもバチは当たらないと思います。
あと、以下の記事をみる限り、Indigoで提供してるのは多分Minimalじゃないっぽいです。tarコマンド最初から入ってたし。
CentOS8 (Minimum) インストールしてとんでもなかったこと一覧 – NorthPage
lsコマンドの挙動は確かに違いますね。

AndroidXでステータスバーから直接入力できるメモアプリを作る。

Androidでステータスバーに直接入力・閲覧できるメモアプリを作る。 – return $lock;
これの続きです。Android 7から10に環境が変わったのでメモアプリを作り直しました。android.supportはもう古いのでAndroidXを使うことにします。
完成したアプリはこんな感じです。画像をクリックするとGIFアニメが再生されます。

ステータスバーメモアプリ

ステータスバーから入力できるメモアプリ


コードが多いので残りは続きを読むに書きます。

使用したライブラリ

  • Android API Level 29
  • androidx.appcompat 1.1.0
  • OkHttp 4.4.0

実装

HTTP通信する場合、メインスレッド以外から通信する必要があるため、前回と同様に以下の構成で実装しました。正しいかどうかは相変わらずわからない。俺たちは雰囲気でコードを書いている。

  • MainActivity
    • Foreground Serviceの起動用にのみ使用するアクティビティ
  • NotificationService(Foreground Serviceとして起動)
    • ステータスバーに通知を常駐させるためのサービス
  • HTTPService(IntentService)
    • NotificationServiceから呼び出されて、HTTP通信するためのサービス
    • 通信完了後にNotificationServiceを再度startする

サービス同士でメッセージをやり取りする際に、IntentにputExtraでStringを詰めています。
前回同様Androidは自信がないので、自分の期待する動作になることと、極端にメモリ食いつぶさないことを目標にして実装します。

実装したコード

build.gradle

// 色々略
dependencies {
    // 色々略
    implementation 'com.squareup.okhttp3:okhttp:4.4.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}

MainActivity

起動初回のみstartServiceするためだけのアクティビティです。それ以外は特になにもしていないです。

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;

import your.package.service.NotificationService;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent i = new Intent(this, NotificationService.class);
        i.putExtra("KEY_EXTRA_STRING", "入力してください");
        startService(i);
    }
}

NotificationService

前回との差分として、通知チャンネルを作成しています。
サービス開始のたびにチャンネルを作成するコードになっていますが、公式チュートリアルに以下の文言があるので多分大丈夫だと思います。心配な方はご自身でデバッグしてください。

既存の通知チャネルをその値を使って新たに作成しようとしても、操作は実行されません。そのためアプリを開始するときに、このコードを実行しても問題ありません。

なお、ダイレクト返信アクションの実装ですが、この記事を書いた時点では公式のチュートリアルに誤りがあり、写経しても動きません

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;

import java.util.Objects;

public class NotificationService extends Service {
    private static final String TAG = "NotificationService";
    private static final int NOTIFICATION_ID = 1;
    private static final String CHANNEL_ID = "CHANNEL_ID"; // 通知チャンネルのIDにする任意の文字列
    private static final String CHANNEL_NAME = "チャンネルの名前"; // 通知チャンネル名

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        Log.d(TAG, "onStartCommand");

        // チャンネルの作成
        // Android 7以下をサポートする場合は、APIレベル26以上でのみ作成するようにSDK_INTで判定する必要がある。
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
        NotificationManager nm = getSystemService(NotificationManager.class);
        Objects.requireNonNull(nm).createNotificationChannel(channel);

        final String replyLabel = "ここに入力してください。";
        RemoteInput remoteInput = new RemoteInput.Builder("KEY_TEXT_REPLY")
                .setLabel(replyLabel)
                .build();

        Intent i = new Intent(this, HTTPService.class);
        PendingIntent replyPendingIntent = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Action action = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_edit,
                replyLabel, replyPendingIntent).addRemoteInput(remoteInput).build();

        Notification newMessageNotification =
                // チュートリアルではNotification.BuilderだがactionがNotificationCompatなのでこちらもcompatにする必要がある
                new NotificationCompat.Builder(this, CHANNEL_ID)
                        .setSmallIcon(android.R.drawable.ic_menu_edit)
                        .setContentTitle("通知のタイトル")
                        .setContentText(intent.getStringExtra("KEY_EXTRA_STRING"))
                        .setPriority(NotificationCompat.PRIORITY_HIGH)
                        .addAction(action).build();

        startForeground(NOTIFICATION_ID, newMessageNotification); // Foreground Service開始

        return START_STICKY;
    }
}

HTTPService

前回同様、OkHttpでAPIと通信したあと、APIから返ってきたレスポンスをputExtraでIntentに詰めてForeground Serviceに返すサービスです。

import android.app.IntentService;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import androidx.core.app.RemoteInput;

import java.io.IOException;
import java.util.Objects;

import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.Response;

import your.package.OkHttp3Singleton;

public class HTTPService extends IntentService {
    private static final String TAG = "HTTPService";
    private static final String API_URL = "https://example.com/your_api";
    public HTTPService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent");

        CharSequence messageText = getMessageText(intent);
        String extraMessage = "";

        if (messageText != null) {

            final String message = messageText.toString();
            Log.d(TAG, message);

            final FormBody.Builder formBodyBuilder = new FormBody.Builder();
            formBodyBuilder.add("message", message);

            final Request request = new Request.Builder()
                    .url(API_URL)
                    .header("YOUR_API_KEY", "VALUE")
                    .post(formBodyBuilder.build())
                    .build();
            try (Response response = OkHttp3Singleton.INSTANCE.getOkHttpClient().newCall(request).execute()) {
                if (response.isSuccessful()) {
                    extraMessage = Objects.requireNonNull(response.body()).string(); // レスポンスをIntentに詰めて別Serviceに渡す
                } else {
                    extraMessage = "エラー:" + response.code() + " が発生しました。";
                }
            } catch (IOException e) {
                extraMessage = "送信失敗。";
            }

        } else {
            Log.d(TAG, "No message.");
        }
        Intent i = new Intent(this, NotificationService.class);
        i.putExtra("KEY_EXTRA_STRING", extraMessage);
        startService(i);
    }

    private CharSequence getMessageText(Intent intent) {
        Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
        if (remoteInput != null) {
            return remoteInput.getCharSequence("KEY_TEXT_REPLY");
        }
        return null;
    }
}

manifest.xml

Permissionに以下が必要です。

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.INTERNET" />

感想

1日動かしましたがメモリ使用量が5MBなので問題ないかなと思います。設定でサイレント通知に持っていけば通常の通知の邪魔にならないので便利ですね。Android 10めちゃくちゃ使いやすくて嬉しいです。