emacs -nw のコピペ事情

### はじめに

今年も残りわずかとなりました。１年前にはまだ対岸の火事ぐらいに思っていたコロナ騒動は、「騒動」の域を超え、新たな時代として語られることになってしまいました。マスクせずに街を闊歩していたあの頃が懐かしいと思うのは私だけではないはずです...

さて、この記事ですが、リモートマシンで emacs -nw 中に、領域選択して kill-ring に入れた情報を TCP で飛ばして、手元の環境のクリップボードに入れる話となります。例えば、リモートの Emacs で領域選択した内容を、手元環境のチャットツールにペーストしようとして、「あれ？！そうか、できないんだった」っていうのは、「CUI Emacs あるある」の一つだと思います。この「あるある」を、bash や nc コマンドを使って、一応解決してみました。<font color="Red">ただ、PoC 的な試みで、残念ながらセキュリティへの配慮が全くできていません。その点だけご留意の上で、読んでいただけたらと思います。</font>

### 環境
いくつかの組み合わせを試してみました。

| 組み合わせ | 手元の環境           | リモートの環境            |
|:-------: |:------------------:|:-----------------------:|
| （１）    | Arch Linux         | Debian 10 + emacs 26.1  |
| （２）    | Arch Linux         | CentOS 7 + emacs 24.3   |
| （３）    | MacOS Big Sur      | Debian 10 + emacs 26.1  |
| （４）    | Windows10 w/ MSYS2 | Debian 10 + emacs 26.1  |

Emacs のバージョンは 24 以降なら大丈夫と思います。一方 nc コマンドは OS によって採用している実装が違い、ちょっとした対処が必要でした。

### Emacs の設定
まず Emacs の設定で必要なことは、interprogram-cut-function にフックを登録することです。

リモート環境で動く Emacs の中で、領域選択やカットした文字列を、手元のマシンに飛ばすための関数 my-cut-function を以下のように定義して、フックに登録します。

``` init.el
(defun my-cut-function (x)
  (with-temp-buffer
    (insert x)
    (shell-command-on-region (point-min) (point-max) "send_region" nil "*Shell Command Output*" t)))
(setq interprogram-cut-function 'my-cut-function)
```

関数 my-cut-function は、外部コマンド send_region を起動して、標準入力からカットした文字列を渡します。ギークはここも elisp で書くのでしょうが、今の私の elisp 力では難しかったです。

### send_region

そんなわけで、標準入力から受け取った文字列を TCP/IP で手元のマシンに飛ばすスクリプト`send_region`を用意しました。

``` sh:send_region
#!/bin/bash

# Linux
cat > /dev/tcp/192.168.0.xxx/5000

# macos
# nc 192.168.0.xxx 5000
```

この例では、手元の環境が192.168.0.xxx というアドレスで、その5000 番ポートに文字列をそのまま飛ばしています。[^1]

### receive_regionn

あとは、手元の PC に飛んできた文字列を nc で受け取り、クリップボードに登録するスクリプト `receive_region` を用意して、手元の環境で動かします。以下のコラムによくまとめられていますが、
> [ネットワーク診断の現場から（netcat編・その1）](http://www.intellilink.co.jp/article/column/security-net01.html)
> 2015年現在、よく利用されているnetcatは以下の3種類です。
> 
> - オリジナル版netcat (netcat-traditional)
> - OpenBSD版netcat
> - Nmap付属netcat

nc の実装が複数あり、今の用途では、5000 番ポートで待ち受ける際、`nc -l -p 5000` と書けばよいケースと、`nc -l 5000` と書かないといけないものがありました。また同じOSでも、どの実装の nc を入れるか選択できるものもあります。 nc の引数は最終的には man ページで確認する必要があります。以下に私の環境での例を挙げます。

##### Linux (Arch Linux w/X-Windows 向け)

``` sh:receive_region
#!/bin/bash

while :
do
    nc -l -p 5000 | xclip -selection c
done
```

#### MacOS
``` sh:receive_region
#!/bin/bash

while :
do
    nc -l 5000 | pbcopy
done
```

#### Windows10 w/MSYS2
MSYS2 利用で、pacman で gnu-netcat をインストールした前提です。

```　sh:receive_region
#!/bin/bash

while :
do
  nc -l -p 5000 | clip
done
```

### まとめ

以下について、サンプル実装を紹介しました。

- Emacs で領域選択した際に、外部コマンドに文字列を渡す init.el 設定
- リモートマシンから、手元の環境に文字列をTCPで送信するスクリプト send_region
- 手元の環境で、文字列を受け取るスクリプト receive_region

冒頭にも書きましたが、セキュリティー面での配慮がありません。例えば、全然関係ないマシンから 5000 ポートに文字列を突っ込まれて、気が付かずにクリップボードの内容をチャットツールにペーストしたら、結果的に同僚をフィッシングサイトに誘導してしまうことも起こるかもしれません。

とはいえ、リモートの emacs の文字列をマウスを触らずにクリップボードに入れられるのは便利だなーというのも正直な感想です。今後は、待受ポートのバインディングを制限する、ssh のポート転送を利用する、認証を入れるなどの対策を考えてみたいと思っています。


[^1]: MacOS Big Sur では `cat > /dev/tcp/...` では動かないので、`nc 192.168.0.xxx 5000` としてやる必要があります。Linux でも nc 使うんじゃだめなのかな？と言われそうですね。nc の実装によって、送った先からの何らかのレスポンスが帰ってこないと終了しないものがあるようです。そのため /dev/tcp を使った例を紹介しました。