Wake on LAN用のPythonスクリプトを作成する（NAT超えWake on LAN[5]）

# はじめに
[前回](https://qiita.com/jjjkkkjjj/items/6f416f27eabbaf04979e)の続きです．
とりあえずやりたいことはこんなんです．

![network_detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/5834caa5-20e1-6878-79a1-01469d50525e.png)

今回は遂に「Wake on LAN用のPythonスクリプトを作成する」です．

- [1][GCPとMyDNSを使って，ドメインを取得する](https://qiita.com/jjjkkkjjj/items/d0370573fcb04deeb91f)
- [2][GCPにSoftether Serverを立てる（iPhone／ラズパイから接続）](https://qiita.com/jjjkkkjjj/items/824998a6106eff941347)
- [3][GCP上のApacheからローカルのラズパイApacheにリバースプロキシする](https://qiita.com/jjjkkkjjj/items/b8363536c488cf51d577)
- [4][ラズパイをルーター化する](https://qiita.com/jjjkkkjjj/items/6f416f27eabbaf04979e)
- [5][Wake on LAN用のPythonスクリプトを作成する．](https://qiita.com/jjjkkkjjj/items/afa4fe637e2c6dae3ecf)


# DesktopPCの設定
前提：Ubuntu18.04です．
まずはWake on LANが実行できるように設定します．

##固定IPの割り当て
```bash
sudo vi /etc/netplan/~~.yaml
```

以下を記入します，

```bash:/etc/netplan/~~.yaml
# Let NetworkManager manage all devices on this system
network:
  version: 2
  renderer: NetworkManager

  ethernets:
    eno1:
       dhcp4: no
       wakeonlan: true # enable wake on lan
       addresses: [192.168.1.**/24] # assign arbitrary address
       gateway4: 192.168.1.1  # your router(raspi)'s address
       nameservers:
         addresses: [8.8.8.8,8.8.4.4] # google's public dns
```

再起動します．

```bash
sudo netplan apply
sudo reboot
```

wake on lan が有効になっているか確認するために`ethtool`をインストールします．

```bash
sudo apt-get install -y ethtool
```

Ethernetの名前をメモします．ついでにIPが固定されているか確認します．

```bash
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eno1<<<<copy<<<<: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.**/24 brd 192.168.1.255 scope global noprefixroute eno1
       valid_lft forever preferred_lft forever
    inet6 fe80::2d8:61ff:fe56:242d/64 scope link
       valid_lft forever preferred_lft forever
```

Wake on LANが有効になっているか確認します．`Wake-on: g`になっていればOKです．

```bash
$ sudo ethtool eno1
~~
Supports Wake-on: pumbg
Wake-on: g # if it's d, your wake on lan setting may be wrong
~~
```
## BIOSの設定
省略します．[起動される側のPCのWOL設定(1/3)：BIOS/UEFIの設定](https://n-archives.net/software/nwol/wol-pc-setting/bios-setting.html)wo
参考にしてください．

## 確認

ラズパイで`ip a`を確認してDesktopに繋いでいるEthernetにipが割り当てられていればOKです．（`UP,LOWER_UP`になっている必要があります．[ありがとうtelcomM](https://unix.stackexchange.com/questions/588973/wake-on-lan-from-custom-router-raspberry-pi/588983#588983)）

```bash
$ ip a
4: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether **:**:**:**:**:** brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global noprefixroute eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::83fa:6dee:9799:9a6e/64 scope link 
       valid_lft forever preferred_lft forever
```

## 確認（wakeonlanをインストールし起動できるか確認）
ラズパイに[wakeonlan](https://packages.debian.org/stable/wakeonlan)をインストールします．

```bash
sudo apt install wakeonlan
```

```bash
wakeonlan -i 192.168.1.255 -p 7 **:**:**:**:**:**
```

これで起動できればOKです．

# mod_wsgi
まず，PythonスクリプトをApacheで動かすことができる`mod_wsgi`と諸々をインストールします．

```bash
sudo apt install python3-dev python3-pip apache2 libapache2-mod-wsgi-py3
sudo apt-get -y install python3 ipython3 python3-flask curl
```

## Pythonスクリプト作成
とりあえず動くPythonスクリプトを作成します．今回はFlaskを用います．

```bash
cd ~
mkdir flask && cd flask
vi app.py
```

```bash:~/flask/app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
    return 'This is vpn server for wake on lan!\n\n'
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

とりあえず，mod_wsgiが動くか確認します．

```bash
$ python3 app.py
$ curl http://localhost:5000 # 別タブで
This is vpn server for wake on lan!

```
”This is vpn server for wake on lan!”と出ればOKです．

## Apacheとmod_wsgiの設定
新たにflask用の設定ファイルを作成します．

```bash
sudo vi /etc/apache2/sites-available/flask.conf
```

今回はGCPから80ポートにリダイレクトされるようにしているので以下のように記入します．

```bash:/etc/apache2/sites-available/flask.conf
<VirtualHost *:80>
    ServerName kado.example.com
    WSGIDaemonProcess flask user={username} group={username} threads=5
    WSGIScriptAlias / /home/{username}/flask/adapter.wsgi
    <Directory /home/{username}/flask/>
        WSGIProcessGroup flask
        WSGIApplicationGroup %{GLOBAL}
        WSGIScriptReloading On
        Require all granted
    </Directory>
</VirtualHost>
```

`flask.conf`を有効化します．

```bash
sudo a2dissite 000-default.conf
sudo a2ensite flask.conf
sudo service apache2 restart
```

adapter.wsgiファイルを作成します．

```bash
vi ~/flask/adapter.wsgi
```

```python:~/flask/adapter.wsgi
import sys
if sys.version_info[0]<3:       # require python3
    raise Exception("Python3 required! Current (wrong) version: '%s'" % sys.version_info)
sys.path.insert(0, '/home/kadorpi/flask/')
from app import app as application # <- app means app of app.py
```

Apacheを再起動します．

```bash
sudo service apache2 restart
```

動いているか確認します．

```bash
$ curl localhost
This is vpn server for wake on lan!

```

これで，PythonスクリプトがApache上で動くようになりました．

# Line bot用のWake on LANスクリプト作成
ここまできたら後少しです．LineからGCPにHTTPリクエストを送り，受け取ったリクエストをラズパイの80ポートにリダイレクトします．そして，ラズパイで受け取ったリクエストをPythonで処理します．

まず，[Line Developer](https://developers.line.biz/console/)にアカウントを登録します．
次にChannelを作成します．

<img width="282" alt="1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/0c84ea26-5fa1-6a78-cec8-3d51f4581ad0.png">

<img width="739" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/09c65e43-5771-3657-308d-559a7ebc7d80.png">

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/a9c35137-3e67-8c2d-4f3e-d7ff4b82b786.png)

Channel Secretを発行し，メモします．

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/88912196-612b-a3d4-54b3-9e69ee6793d1.png)

Channel Access Tokenを発行し，メモします．

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/1cb9a040-3811-35c0-09ae-f43d2faa8a4e.png)

＞Response settingsからAuto-replyなどを適宜設定します．

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/095e1be4-56c7-b813-f78b-d93eb35dc5a9.png)

＞Message APIからWebhookURLを入力します．今回は`https://{domain name}/wake-on-lan`とします．

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253935/1b03b47e-592a-019c-db71-1f94a7de6ffb.png)

これでLINE Botの設定は終わりです．

## mod_wsgiの再設定
環境変数として，先ほどメモしたChannel SecretとChannel Access Tokenを設定します．

```bash
vi ~/flask/adapter.wsgi
```

以下を追記します．

```python:~/flask/adapter.wsgi
import os
os.environ['LINE_CHANNEL_SECRET'] = 'fuga'
os.environ['LINE_CHANNEL_ACCESS_TOKEN'] = 'hogehoge'
```

## Pythonスクリプト

必要なモジュールをインストールします．

```bash
pip3 install line-bot-sdk flask
```

pythonコードは以下のようにします．

```python:
import os, sys
from flask import Flask, request, abort
app = Flask(__name__)

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    TextMessage, TextSendMessage, MessageEvent
)

# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

@app.route('/')
def show_web():
    return 'This is vpn server for wake on lan!\n\n'

@app.route('/wake-on-lan', methods=['POST'])
def wake_on_lan():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'ok'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = event.message.text

    replies = []
    if re.compile("\s*(check)\s*", re.IGNORECASE).search(message):
        result = confirm()
        replies += [TextSendMessage(result)]

    elif re.compile("\s*(kick)\s*", re.IGNORECASE).search(message):
        if confirm() == 'Awake':
            replies += [TextSendMessage('Already awake')]
        else:
            result = kick()
            replies += [TextSendMessage(result)]

    buttons_template = ButtonsTemplate(
        title='usage', text='Tap below buttons', actions=[
            MessageAction(label=m, text=m) for m in message_list
        ])

    template_message = TemplateSendMessage(alt_text='Usage', template=buttons_template)
    replies += [template_message]

    line_bot_api.reply_message(event.reply_token, replies)


def confirm():
    hostname = "192.168.1.**"
    response = os.system("ping -c 1 " + hostname)
    # and then check the response...
    if response == 0:
        pingstatus = "Awake"
    else:
        pingstatus = "Sleeping"

    return pingstatus

def kick():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    s.sendto(b'\xFF' * 6 + b'\x**\x**\x**\x**\x**\x**' * 16, ('192.168.1.255', 7))
    s.close()

    return 'kicked'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

少し説明を入れると，まず以下でLINE用のChannel SecretとChannel Access Tokenを読み込みます．

```python
# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None:
    print('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    print('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)
```

以下は，Web表示用に一応加えました．


```python
@app.route('/')
def show_web():
    return 'This is vpn server for wake on lan!\n\n'
```

ここに上記で設定したWebhookのリクエスト処理を書きます．

```python
@app.route('/wake-on-lan', methods=['POST'])
def wake_on_lan():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'ok'
```

そして，最後にメッセージで"kick"が来たら，MagicPacketの送信，"check"が来たらPingで起動できているかを確認する処理をここで書いています．


```python
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = event.message.text

    replies = []
    if re.compile("\s*(check)\s*", re.IGNORECASE).search(message):
        result = confirm()
        replies += [TextSendMessage(result)]

    elif re.compile("\s*(kick)\s*", re.IGNORECASE).search(message):
        if confirm() == 'Awake':
            replies += [TextSendMessage('Already awake')]
        else:
            result = kick()
            replies += [TextSendMessage(result)]

    buttons_template = ButtonsTemplate(
        title='usage', text='Tap below buttons', actions=[
            MessageAction(label=m, text=m) for m in message_list
        ])

    template_message = TemplateSendMessage(alt_text='Usage', template=buttons_template)
    replies += [template_message]

    line_bot_api.reply_message(event.reply_token, replies)


def confirm():
    hostname = "192.168.1.**"
    response = os.system("ping -c 1 " + hostname)
    # and then check the response...
    if response == 0:
        pingstatus = "Awake"
    else:
        pingstatus = "Sleeping"

    return pingstatus

def kick():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    s.sendto(b'\xFF' * 6 + b'\x**\x**\x**\x**\x**\x**' * 16, ('192.168.1.255', 7))
    s.close()

    return 'kicked'
```

Apacheを再起動します．

```bash
sudo service apache2 restart
```




# おわり
後半，疲れてだれてしまいました．．．
さらに備忘録的に書いているのでわかりづらいかもしれません．．．
とりあえず全部書けたので良かったです。参考になれば幸いです．

![動画](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F253935%2Fd3d36bff-e735-470e-c122-1a129c06022f.gif?ixlib=rb-1.2.2&auto=format&gif-q=60&q=75&s=6c91fc3d3428fe4820d7e50edf200826)


# 参考
[How To Use Apache HTTP Server As Reverse-Proxy Using mod_proxy Extension](https://www.digitalocean.com/community/tutorials/how-to-use-apache-http-server-as-reverse-proxy-using-mod_proxy-extension)