カテゴリー: Web開発

  • SESAME3 に対応した Web API の使い方

    いつの間にかしれっと Web API が SESAME3 に対応していたので、実際に使ってみる手順を記録します。

    ※しばらく随時修正しています

    ドキュメント

    SESAME3 の UUID 確認

    公式アプリで操作したい鍵を設定画面を開いて確認します。

    API キーの取得

    ダッシュボードにログインします。
    ログインの仕方はアプリ版と同じで、メールで認証番号が届いて、それを入力するだけです。

    file

    file

    file

    file

    「API_KEY」を開くと確認できます。
    「CLIENT_KEY」は今のところ使いません。

    SESAME3 の状態取得

    Firefox の拡張機能 RESTer で動作確認します。

    file

    成功すると200でJSONが返ってきます。
    パラメータが間違っていたりすると502 Bad Gatewayになります。

    • batteryPercentage: 電池残量
    • batteryVoltage: 電池電圧
    • position: サムターン角度
    • CHSesame2Status: 状態
    • timestamp: 更新された時刻(1970-01-01 00:00:00:000からの経過ms)

    SESAME3 の履歴取得

    Firefox の拡張機能 RESTer で動作確認します。

    file

    成功すると200でJSONが返ってきます。
    パラメータが間違っていたりすると502 Bad Gatewayになります。

    SESAME3 の解錠・施錠操作

    secret key の取得

    公式アプリから鍵のシェアを行って表示されるQRコードを読み取ります。

    iPhone の場合は「ショートカット」アプリで新規ショートカットを作成し、「書類」の「QR/バーコードをスキャン」と「Quick Lookで表示」を追加し、「QR/バーコード」を「テキストとして」にしておくとQRコードの中身をテキストのまま得られます。

    URLは次のような形式になっていると思います。

    ssm://UI?t=sk&sk=AXXXXXXXX省略)XXXXXXXXXXX&l=0&n=%%%%%%%%%%

    この sk= の値をコピーしておいて、Base64 decode した文字列の1-17文字が secret key になります。
    1-17文字といってもindexでの[1:17](JavaScriptだとslice(1, 17))なので、実際は2文字目から17文字目までですね。

    Pythonが使える環境であれば、次のようにして対話モードでサクッと変換できます。

    $ python
    Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)] on win32
    Type "help", "copyright", "credits" or "license" for more information.
    >>> b64 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # skの値(sk=の後から&前まで)
    >>> import base64
    >>> base64.b64decode(b64)[1:17].hex()
    'abcdef0123456789hogehoge'

    動作確認

    CMACの生成が手間だったのでPythonで行っています。
    pipでpycryptodome, requestsをインストールしておきます。

    $ pip install pycryptodome, requests
    import datetime, base64, requests, json
    from Crypto.Hash import CMAC
    from Crypto.Cipher import AES
    
    uuid = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
    secret_key = '0123456789abcdef0123456789abcdef'
    api_key = 'XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx'
    
    # 鍵の操作(toggle/lock/unlock)
    cmd = 82    # 88/82/83 = toggle/lock/unlock
    
    # 履歴に残す内容
    history = 'Locked (Web API)' # 半角21文字/全角不明
    history = base64.b64encode(history.encode()).decode()
    
    # HTTP header
    headers = { 'x-api-key': api_key }
    
    # signの生成
    cmac = CMAC.new(bytes.fromhex(secret_key), ciphermod=AES)
    message = int(datetime.datetime.now().timestamp()).to_bytes(4, 'little', signed=False)[1:4]
    cmac.update(message)
    sign = cmac.hexdigest()
    
    # 鍵の操作
    url = f'https://app.candyhouse.co/api/sesame2/{uuid}/cmd'
    
    body = {
        'cmd': cmd,
        'history': history,
        'sign': sign
    }
    res = requests.post(url, json.dumps(body), headers=headers)
    print(res.status_code, res.text)

    参考

  • Vue CLI のインストールからプロジェクトの作成まで

    Vue CLI のインストールからプロジェクトの作成まで

    自分用のメモです。

    インストール

    遅いです。

    $ npm install -g @vue/cli

    空プロジェクトの作成

    $ vue create project-name

    開発サーバの起動

    $ cd project-name
    $ npm run serve

    (参考)Git からプロジェクトをダウンロードしたとき

    $ unzip project-name.zip
    $ cd project-name
    $ npm install

    参考

  • reCAPTCHA v3 を Python CGI で使う

    reCAPTCHA v3 を Python CGI で使う

    以前に reCAPTCHA v2 の導入記事を書きましたが、今回は reCAPTCHA v3 です。v2 の画像選択が手間になってきて、よりシンプルに扱える v3 に更新しようと思ったのがきっかけです。

    チェックボックスにチェックする v2 と異なり、v3 はフォームに埋め込むものがありません。JavaScript で token を取得するところまでは一緒なのですが、その token を自分でフォームに埋め込む必要があります。

    reCAPTCHA への登録の仕方は v2 も v3 も同じなので、前回の記事に譲ります。

    クライアントサイド

    head 要素内に次のコードを埋め込みます。

    <script src="https://www.google.com/recaptcha/api.js?render=SITE_KEY"></script>

    SITE_KEY の部分は各々発行されたものに置換えてください。

    次に、どこでも良いので次のコードを埋め込みます。head 要素内でも良いですし、body の最後でも問題ありません。

    <script>
    grecaptcha.ready(function() {
      grecaptcha.execute('SITE_KEY', {action: 'action_name'})
      .then(function(token) {
        document.getElementById('g-recaptcha-response').value = token;
      });
    });
    </script>

    ‘action_name’ は名前分けしておくと、admin console で分析が行えるようです。

    最後に、フォーム要素の中に次の隠し要素を埋め込んでおきます。

    <input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" value="">

    先ほどの JavaScript が実行されると、token がこの要素の value に入ります。

    サーバーサイド (Python2)

    今回はお手軽さで cgi.FieldStorage() と urllib を使います。

    import cgi
    import urllib, urllib2
    import json
    
    # フォームの取得
    form = cgi.FieldStorage()
    
    # g-recaptcha-responseの値を取得
    response = form.getfirst('g-recaptcha-response', '')
    
    # APIに照合を行う
    params = urllib.urlencode({'secret': SECRECT_KEY, 'response': response})
    req = urllib2.Request('https://www.google.com/recaptcha/api/siteverify', params)
    result = json.loads(urllib2.urlopen(req).read())

    返ってくる json には次の値が含まれています。

    {u'action': u'action_name', u'score': 0.9, u'hostname': u'kuratsuki.net', u'challenge_ts': u'2018-12-06T06:51:20Z', u'success': True}
    success
    True|False 正しい token が送られたかどうかの判定
    score
    0.0-1.0 bot(0.0) に近いか人間(1.0)に近いかを数値で表します
    action
    string action 名
    challenge_ts
    yyyy-MM-dd’T’HH:mm:ssZZ タイムスタンプ
    hostname
    string reCAPTCHA が行われたホスト名
    error-codes
    [] エラーがあったときのエラーのリスト

    サーバーサイドではこの score を用いて処理を分岐させれば良いようです。

    参考

  • Debian 9 Stretch で Docker のインストール

    Debian 9 Stretch で Docker のインストール

    Debian 9 Stretch に Docker をインストールして動作確認を行うまで。

    $ sudo apt update
    $ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common
    $ curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

    /etc/apt/sources.list に次の一行を追加する。

    deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable

    もう一度 apt update して更新を行ったあとにインストールを行います。

    $ sudo apt update
    $ sudo apt install docker-ce

    動作確認に hello-world の image を run してみます。

    $ sudo docker run hello-world
    Unable to find image 'hello-world:latest' locally
    latest: Pulling from library/hello-world
    d1725b59e92d: Pull complete
    Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
    Status: Downloaded newer image for hello-world:latest
    Hello from Docker!
    This message shows that your installation appears to be working correctly.
    To generate this message, Docker took the following steps:
    The Docker client contacted the Docker daemon.
    The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
    The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
    The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.
    To try something more ambitious, you can run an Ubuntu container with:
    $ docker run -it ubuntu bash
    Share images, automate workflows, and more with a free Docker ID:
    https://hub.docker.com/
    For more examples and ideas, visit:
    https://docs.docker.com/get-started/

    ローカルのイメージを表示

    $ sudo docker images
    REPOSITORY TAG IMAGE ID CREATED SIZE
    hello-world latest 4ab4c602aa5e 2 months ago 1.84kB

    動作中のコンテナを表示

    $ sudo docker ps
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

    全てのコンテナを表示

    $ sudo docker ps -a
    CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
    4d4807dccc3c hello-world "/hello" 8 minutes ago Exited (0) 8 minutes ago friendly_chandrasekhar

    コンテナの削除

    CONTAINER ID を指定します。

    $ sudo docker rm 4d4807dccc3c
    4d4807dccc3c

    ローカルのイメージを削除

    イメージの名前か IMAGE ID を指定します。

    $ sudo docker rmi hello-world
    $ sudo docker rmi 4ab4c602aa5e

    参考

  • Debian 9 Stretch で locale の変更

    Debian 9 Stretch で locale の変更

    $ localectl
    System Locale: n/a
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105
    $ localectl list-locales
    C.UTF-8
    en_US.utf8
    $ sudo localectl set-locale LANG=en_US.UTF-8
    $ localectl
    System Locale: LANG=en_US.UTF-8
    VC Keymap: n/a
    X11 Layout: us
    X11 Model: pc105
  • Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python WSGI で毎回 encode(‘utf-8’) するのが面倒

    Python の WSGI プログラムで出力をするたび

    yield '<h1>テスト</h1>'.encode('utf-8')
    yield '<p>あいうえお</p>'.encode('utf-8')

    と encode(‘utf-8’) を付けていましたが、どう考えてもこれは手間だし無駄な気がします。Flask を使えばいいと言われたらお終いなので、何か手はないかと考えてみました。

    Python にはデコレータ(decorator) という仕組があって、任意の関数の前後に好きな処理を追加することができます。関数も第一級オブジェクトである Python の特性をうまく利用した素敵機能ですね。

    yield を扱った decorator の例があまり見つからなくてやや苦戦しましたが、次のような decorator で実際に動作することを確認しました。

    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper

    前回の WSGI でフォームを扱うプログラムに適用させてみたのが次のコードです。

    # coding: utf-8
    
    from urllib.parse import parse_qsl
    
    def encode_utf8(func):
    	def wrapper(*args, **kwargs):
    		for r in func(*args, **kwargs):
    			yield r.encode('utf-8')
    	return wrapper
    
    @encode_utf8
    def application(env, res):
    	res('200 OK', [('Content-type', 'text/html; charset=utf-8')])
    
    	# Form
    	yield '<form method="get" accept-charset="UTF-8"><p><input type="text" name="get" value=""><input type="submit" value="GET"></form>'
    	yield '<form method="post" accept-charset="UTF-8"><p><input type="text" name="post" value=""><input type="submit" value="POST"></form>'
    
    	# Request method
    	method = env.get('REQUEST_METHOD')
    	yield '<p>Method: {}</p>'.format(method)
    
    	# Get query string
    	if method == 'POST':
    		wsgi_input = env['wsgi.input']
    		query = wsgi_input.read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')
    	else:
    		query = env.get('QUERY_STRING', '')
    	yield '<p>Query: {}</p>'.format(query)
    
    	# Output form fields
    	form = parse_qsl(query)
    	for k, v in form:
    		yield '<p>{} = {}</p>'.format(k, v)

    行数は変らないのでぱっと見の変化少ないですが、書き忘れて Internal Server Error をもらう率は少し減りますね。

  • Python WSGI でフォームのデータを取得

    Python WSGI でフォームのデータを取得

    TL; DR

    POST

    query = env['wsgi.input'].read(int(env.get('CONTENT_LENGTH', 0))).decode('utf-8')

    GET

    query = env.get('QUERY_STRING', '')

    parse and split

    # dictionary
    form = urllib.parse.parse_qs(query)
    # list
    form = urllib.parse.parse_qsl(query)
    (さらに…)