Charlesを快適に使うためにやっていること

Charles Web Debugging Proxy とても便利ですよね。

今でこそ、StethoFlipper などの、Androidアプリ側に仕込むライブラリでも事足りるようにもなってきましたが、通信内容の改ざんや特定のリクエストのみ他のサーバに転送したりなど、多岐にわたる機能がとても便利なため、どうしてもこれらのライブラリでは手が届かないところは、Charlesを使い続けています。

そもそも Charles って何なの?どう使うの?といった内容のブログは山程あるので、そういった方はそちらを見ていただくとして、私が紹介したいのは、Charlesをよりストレスフリーに使うためのテクニックをご紹介したいと思います。mitmproxy など、他のプロキシツールでも使えるテクニックです。

Charlesを使いながら面倒だなあと思うこと

Charlesを日常で使っていて面倒なことといえば、プロキシの設定じゃないでしょうか。AndroidWiFiのプロキシを設定しようと思った場合、設定アプリのWiFiの画面を表示して、いま接続しているWiFiのオプションを開き、プロキシを設定するモードにして、IPアドレスを設定して、ポート番号を指定して… となかなかステップ数が多く、モバイルの小さい画面でIPアドレスを打つのはなかなかストレスフルです。そもそもIPアドレスを設定するためには、プロキシサーバになる母艦のIPアドレスを知る必要があるので、ターミナルでifconfig とか打たないといけないですね*1

逆にプロキシを使いたくない時もまた面倒だなあと思うことがあります。プロキシの設定がされたままではAndroid側は通信をすることができないので、母艦でCharlesを立ち上げ続けておく必要があります。Charlesはそんなに軽いアプリケーションというわけでもなく、必要ないときには起動しておきたくないので、モバイル側のプロキシの設定を消す必要があります。 ここで消したら再び使いたいときに、また奥まった機能の小さな画面でIPアドレス打ったりしないといけません。

PAC

日々プロキシの設定をしたり消したりしているうちに、あるオプションに気づきました。

プロキシの自動設定
プロキシの自動設定

プロキシの種類を指定するところに「プロキシの自動設定」とあります。なんとも甘味な響きに胸を打たれ、なんなのかと調べました。 調べてみると、これはプロキシに関する設定を定義として配布できる仕組みだと分かりました。

ja.wikipedia.org

起源はNetscape時代から始まっており、なかなか歴史があるものなのですが、全然知りませんでした。 これを使えば小さい画面でポチポチプロキシの設定することから開放されそうです。

PACをどう書くのか

PACファイルはhttp(s) によってアクセスできる場所に置いておく必要があります。Amazon S3Google Cloud Storageが手軽で適当だと思いました。 そこに次のようなファイルを置くとプロキシが有効になります。

function FindProxyForURL(url, host) {
  if (isPlainHostName(host) || isInNet(host,"127.0.0.1", "255.255.255.255")) {
    return "DIRECT";
  } else {
    return "PROXY 192.168.10.2:8888; DIRECT";
  }
}

PACファイルはJavascriptで記述します。FindProxyForURL という関数によってプロキシを使うかどうかを判定します。 この記述内容であれば、「通信先がローカルホストでなければ 192.168.10.2:8888 をプロキシとして使う」という内容になっています。

逆にプロキシを使わない場合は、次のようなファイルにしておきます。

function FindProxyForURL(url, host) {
  return "DIRECT";
}

DIRECT という文字列を返すと、プロキシを使いません。

詳しい書式は割愛します。プロキシ自動設定ファイル - HTTP | MDN あたりが詳しいです。

あとは、このファイルにアクセスするためのURLをモバイル側のプロキシの設定として設定するだけです。

プロキシの設定
プロキシの設定

日常でPACを使うために

Amazon S3Google Cloud Storageなどにファイルを置くようにすればファイルの更新も容易ですが、IPアドレスを調べたり、ファイルを更新したりといった作業も面倒だと思い、ツールを作りました。

github.com

コマンド一つでPACファイルを通してプロキシの有効/無効を切り替えることができるツールです。 局所的にそこだけコマンドを紹介すると、次のようなコマンドになります。

$ pac enable # プロキシ有効化
$ pac disable # プロキシ無効化

詳しい pacコマンドの設定や使い方は次節に書きます。

pac コマンドの使い方

設定

pacコマンドを使うためには、まず設定をする必要があります。これは、pacファイルをどこに置いておくか、という設定なので、誰しもがやる必要があります。 例えば Amazon S3にファイルを置く場合は次のようになります。

---
s3:
  bucket: your-bucket-name
  path: /

だいたい読み取れるんじゃないかと思いますが、この内容で、 s3://your-bucket-name/yout-name.pac という場所にPACファイルができます*2。 自分の環境に合わせてバケット名やパスを設定できます。

プロキシの有効化

次のコマンドで有効になります。

$ pac enable

内部で aws コマンドを使っているため、あらかじめ認証が通るようにしておく必要があります。

プロキシの無効化

$ pac disable

補足

  • Google Cloud Storageのことも書きましたが、実はサポートしていません。自分が使う範囲で使えるなーと思ったので、公開しました。気が向いたらGCSを対応するかもしれませんが、PR貰えれば嬉しいです。
  • Androidを中心に例を挙げましたが、もちろんiOSでも使うことができます。

最後に

PACファイルという存在を知ってから、プロキシを設定しなくてはならない、解除しなくてはならない、といったシチュエーションでだいぶストレスが減りました。通信内容を確認したいと思ったときに手軽にそれを使うことができるのは、とてもよいものですね。気持ちよく開発をすることができます。

*1:毎回この設定をしなくてはならない、というわけではありません。DHCPによって概ね同じIPがわりふられるようになっているんじゃないかと思うので、設定しなくていいシチュエーションの方がほとんどです。

*2:yout-name.pac にはログインユーザーの名前が入ります

通知センターの通知を全部消す

通知の種類をアラートからバナーに変更するとすぐに消えてしまって見逃しがありそう…ってことで、だいたいの通知はアラートにして、自動的に消えないようにしています。 ですが、ちょっと離席したときに通知が溜まっていると、それを全部ポチポチ消すのは面倒ですね。

そこで全部消すためのスクリプトをAlfred workflowにしています。 動作イメージはこんな感じ。

f:id:tomorrowkey:20180818144621g:plain

シュッと消えてくれて便利です。 こちらからダウンロードできますが、macOSの設定を英語にしていないと動きません。

github.com

ここのClose閉じるに変更すると日本語でも動くので、その場合はビルドしなおして alfredworkflowファイルを作り直してください。

https://github.com/tomorrowkey/alfred-workflows/blob/master/clear_notification/clear.applescript#L5

cloneしていないリポジトリのmasterのハッシュを調べる

git-ls-remote を使う

$ git ls-remote https://github.com/tomorrowkey/adb-peco master
788d83985e44f747a785cb948ac75fe4a048862d    refs/heads/master

もちろんgithubじゃなくてもいいからandroidのframework/baseのmasterを取得する場合はこんな感じ

$ git ls-remote https://android.googlesource.com/platform/frameworks/base master
d68f003c67ca6364202a57f0c695012d4ea4571e    refs/heads/master

オプションなど

$ man git-ls-remote
GIT-LS-REMOTE(1)                                                                                                                                              Git Manual                                                                                                                                             GIT-LS-REMOTE(1)

NAME
       git-ls-remote - List references in a remote repository

SYNOPSIS
       git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
                     [-q | --quiet] [--exit-code] [--get-url] [--sort=<key>]
                     [--symref] [<repository> [<refs>...]]

DESCRIPTION
       Displays references available in a remote repository along with the associated commit IDs.

OPTIONS
       -h, --heads, -t, --tags
           Limit to only refs/heads and refs/tags, respectively. These options are not mutually exclusive; when given both, references stored in refs/heads and refs/tags are displayed.

       --refs
           Do not show peeled tags or pseudorefs like HEAD in the output.

       -q, --quiet
           Do not print remote URL to stderr.

       --upload-pack=<exec>
           Specify the full path of git-upload-pack on the remote host. This allows listing references from repositories accessed via SSH and where the SSH daemon does not use the PATH configured by the user.

       --exit-code
           Exit with status "2" when no matching refs are found in the remote repository. Usually the command exits with status "0" to indicate it successfully talked with the remote repository, whether it found any matching refs.

       --get-url
           Expand the URL of the given remote repository taking into account any "url.<base>.insteadOf" config setting (See git-config(1)) and exit without talking to the remote.

       --symref
           In addition to the object pointed by it, show the underlying ref pointed by it when showing a symbolic ref. Currently, upload-pack only shows the symref HEAD, so it will be the only one shown by ls-remote.

       --sort=<key>
           Sort based on the key given. Prefix - to sort in descending order of the value. Supports "version:refname" or "v:refname" (tag names are treated as versions). The "version:refname" sort order can also be affected by the "versionsort.suffix" configuration variable. See git-for-each-ref(1) for more sort options,
           but be aware keys like committerdate that require access to the objects themselves will not work for refs whose objects have not yet been fetched from the remote, and will give a missing object error.

       -o <option>, --server-option=<option>
           Transmit the given string to the server when communicating using protocol version 2. The given string must not contain a NUL or LF character. When multiple --server-option=<option> are given, they are all sent to the other side in the order listed on the command line.

       <repository>
           The "remote" repository to query. This parameter can be either a URL or the name of a remote (see the GIT URLS and REMOTES sections of git-fetch(1)).

       <refs>...
           When unspecified, all references, after filtering done with --heads and --tags, are shown. When <refs>... are specified, only references matching the given patterns are displayed.

EXAMPLES
           $ git ls-remote --tags ./.
           d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
           f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
           7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3
           c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
           0918385dbd9656cab0d1d81ba7453d49bbc16250        refs/tags/junio-gpg-pub
           $ git ls-remote http://www.kernel.org/pub/scm/git/git.git master pu rc
           5fe978a5381f1fbad26a80e682ddd2a401966740        refs/heads/master
           c781a84b5204fb294c9ccc79f8b3baceeb32c061        refs/heads/pu
           $ git remote add korg http://www.kernel.org/pub/scm/git/git.git
           $ git ls-remote --tags korg v\*
           d6602ec5194c87b0fc87103ca4d67251c76f233a        refs/tags/v0.99
           f25a265a342aed6041ab0cc484224d9ca54b6f41        refs/tags/v0.99.1
           c5db5456ae3b0873fc659c19fafdde22313cc441        refs/tags/v0.99.2
           7ceca275d047c90c0c7d5afb13ab97efdf51bd6e        refs/tags/v0.99.3

SEE ALSO
       git-check-ref-format(1).

GIT
       Part of the git(1) suite

Git 2.18.0                                                                                                                                                    06/21/2018                                                                                                                                             GIT-LS-REMOTE(1)

配列の要素を一意にする

RubyでいうところのArray#uniqみたいなもの

let array = [1, 1, 2, 4, 1, 3, 2]
array.enumerated().compactMap({ index, item in array.index(of: item) == index ? item : nil })

それぞれの「index」と「arrayからindex(of:) で取得したindex」が一致した場合のみ要素が残るようにしている。 つまり最初に出現した要素だけが残るようになっている。 毎回書くののも面倒なので、extensionしておくと便利

Closureの即時実行を使ったスコープの切り分け

Swiftのletはimmutableな変数宣言で、一度値を入れたらその後に変更することができない。

let hoge = "hoge"
hoge = "fuga" // コンパイルエラー

これは変数宣言のときに値を入れないといけない、という制限ではなく、その変数が使われるまでに値が入っていれば問題ない。 ただし、その場合は型の宣言を省略できない。

let hoge: String
hoge = "hoge" // OK

「その変数が使われるまでに値が入っていれば問題ない。」というのは例えばif文やswitch文を使っても大丈夫

let hoge: String
if arc4random() % 2 == 0 {
    str = "even"
} else {
    str = "odd"
}
debugPrint(str) // "even" もしくは "odd"が出力される

たまに処理が複雑で使い捨て変数を割り当てないといけないことがある。

let array = Array(1...10)
var sum = 0
array.forEach({ sum += $0 })
let average = sum / array.count

このときsum変数は一時的な変数で、averageを求めたら必要なくなる。このexampleではそんなことないんだけど、使い捨ての変数名を考えるが面倒で、手が止まってしまうことがしばしばあり、違うスコープを切りたい時に有効なのがクロージャだ。

let array = Array(1...10)
let average: Int = {
    var sum = 0
    array.forEach({ sum += $0 })
    return sum / array.count
}()

クロージャ内部は別のスコープなので、使い捨ての変数は本当に使いたい部分でだけ有効になる。 まぁそもそもこのexampleではreduceを使えばいい話なんだけど…

let average = array.reduce(0) { sum, i in sum + i } / array.count

Viewに関するコードを書いていると使い捨ての変数を定義したくなることがあるので、そういうときに使うと便利。

forEachでインデックスを使いたい

配列をすべて舐めるような処理をする時はforEachを使うけど、この時indexを使うことができない

let array = ["a", "b", "c", "d"]
array.forEach { string in
  debugPrint(string)
}

そんなときはArray#enumerated() を使えばindexも使うことができる

let array = ["a", "b", "c", "d"]
array.enumerated().forEach({ index, string in
    debugPrint("\(index): \(string)")
})

// "0: a"
// "1: b"
// "2: c"
// "3: d"

UITextFieldで入力文字を制限する

UITextFieldDelegate を使う

textField(_:shouldChangeCharactersIn:replacementString:) - UITextFieldDelegate | Apple Developer Documentation

例えばIntにパースできる値だけにしたければ、次のようなコードになる

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return Int(string) != nil
    }
}

ただ、このdelegateメソッドの引数である string は入力された値だけなので、UITextFieldに入る値を数値だけに制限したい場合は、これだけでは不十分。 range 引数に、UITextField のキャレットなどの文字選択位置が渡されるので、それで置換してあげればいい。

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let currentString = textField.text, let _range = Range(range, in: currentString) {
            let newString = currentString.replacingCharacters(in: _range, with: string)
            return Int(newString) != nil
        } else {
            return false
        }
    }
}