Okada Hiroshi の blog

typo が多いです

Jupyter Widgets (ipywidgets) のパスワード入力フィールド

今日、別件で検索中に StackOverflow で見つけたのですが、Jupyter 上の Widgets に Password 入力フィールドが存在していました。

stackoverflow.com

ドキュメント には記述されていなのですが、ソースコードには ずいぶん前から入っているようです。

github.com

他の Widgets と同じように widgets.Password(value = '', placeholder='パスワードを入力') のようにして使えます。

以下は、サンプルとして作ったはてなブログのエントリー一覧を表示する Google Colaboratory のノートブックですが、API KEY 入力フィールドを非表示にできるので、スクリーショット等を公開する時にマスクする必要がなくて便利だと思います。

f:id:OkadaHiroshi:20190505172639p:plain

https://colab.research.google.com/gist/HiroshiOkada/b43dea3d79c574dcc0fb119a86a8c6ae/hatenablog.ipynb

はてなダイアリーをインポートしたらカテゴリーがカオスになったので Python と テキストエディタ(Vim) の力を借りて修正しました

はてなダイアリー」が終了するので、インポート機能を使って、過去の記事を「はてなブログ」にインポートしました。

ところが、「はてなダイアリー」と「はてなブログ」でカテゴリーが統一されていなかったため、カテゴリーがカオスになってしまいました。

また、初期には「はてなダイアリー」を日記モードで書いていたため、タイトルが無い記事も多数存在していました。

これらを「はてなブログ」の Web 画面上から変更することは(僕には)苦痛だったので、 一括して変更するために、記事(entry)のタイトルとカテゴリーを読み書きするツールを Python で作成し、 テキストエディタで編集して変更しました。

github.com

まず、get-tc.py というスクリプトでタイトルとカテゴリーを読み出しました。

python get-tc.py --api-key=<APIキー> <ルートエントリーポイント> <出力ファイル名>

APIキーと、ルートエントリーポイントは、はてなブログの設定画面から取得できます。

Screenshot (API_KEY)

ファイルの中身は yaml 形式で、記事(entry)の配列になっており、ひとつの記事は以下の用になっています。

- categories:
  - カテゴリ1
  - カテゴリ2
  entry_id: エントリID
  link: 記事のURL
  published: 最初に書いた日付
  summary: 記事のサマリー
  title: 記事のタイトル

テキストエディタでこのファイルを書き換えました。 なお、タイトルとカテゴリ以外の項目は変更しても反映しないようにしました。

編集が終わったら、以下のコマンドで更新しました。

python update-tc.py --api-key=<APIキー> <ルートエントリーポイント> <入力ファイル名>

なお、エディタは vim をつかって次のような使い捨ての vimscript を作成し読み込んでから編集しました。

nnoremap q#  o- C#<ESC>
nnoremap qB  o- bash<ESC>
nnoremap qC  o- C<ESC>
nnoremap qD  o- Docker<ESC>
nnoremap qJ  o- JavaScript<ESC>
nnoremap qP  o- Python<ESC>
nnoremap qR  o- Ruby<ESC>
nnoremap qS  o- SSL<ESC>
nnoremap qd  o- 読書・マンガ・アニメ・映画<ESC>
nnoremap qi  o- IT<ESC>
nnoremap qk  o- 購入<ESC>
nnoremap ql  o- 生活・健康・旅行<ESC>
nnoremap qm  o- MS Office・Googleドキュメント<ESC>
nnoremap qn  o- ニュース・時代<ESC>
nnoremap qt  o- 投資・経済<ESC>
nnoremap q/  /^- categories<CR>

https (ssl) サーバーのサンプル

ローカルネットで開発する時に使用するワイルドカード SSL 証明書の取得方法の記事を書いたので、いくつかの言語で簡単な https サーバープログラムを記述しようと思います。 最近の言語は標準ライブラリや定番のライブラリが充実しているので簡単に https サーバーを書くことができます。

コンテンツ

なお、折角 https サーバーで配信するのですから、加速度センサーを使うサンプルコンテンツにしようと思います。

以下のように Android からアクセスするとスマホを回転させても頭が上でありつづけます。

残念ながら iOSバイスでは動作しません。 PC では API は使えるようですが物理的に加速度センサーがほぼ付いていないので使えません。 Accelerometer | MDN

とりあえず簡単のために index.html に JavaScriptCSS も書いてしまいました。 ( index.htmlpublic という名前のフォルダーに入れました。 )

<!DOCTYPE html>
<html lang="ja">

<head>

<meta name="viewport" content="width=device-width,initial-scale=1">
<meta charset="UTF-8">
<title>Stick figure</title>

<style>
#stick-figure {
  width: 50%;
  margin: 0 auto;
  transform: rotate(0rad);
  transform-origin: center;
}
#message {
  color: red;
  font-size: 2rem;
}
</style>

</head>

<body>

<svg viewBox="0 0 200 200" id="stick-figure">
  <g stroke="green" fill="none" stroke-width="5">
    <circle cx="100" cy="30" r="20" />
    <line x1="100" y1="50" x2="100" y2="110" />
    <polyline points="80,190 100,110 120,190" />
    <polyline points="40,75 100,65 160,75" />
  </g>
</svg>

<p id="message"></p>

<script>
const main = () => {
  const stickFigure = document.getElementById('stick-figure');
  const message = document.getElementById('message');

  if (!('Accelerometer' in window)) {
    message.innerHTML = 'no Accelerometer';
    return;
  }
  navigator.permissions.query({ name: 'accelerometer' }).then(result => {
    if (result.state !== 'granted') {
      message.innerHTML = result.state;
      return;
    }
    const acc = new Accelerometer({ frequency: 60 });
    acc.addEventListener('reading', e => {
      const { x, y } = acc;
      const rad = -Math.atan2(y, x) + Math.PI / 2.0;
      stickFigure.style.transform = `rotate(${rad}rad)`;
    });
    acc.start();
  });
};

main();
</script>

</body>
</html>

Ruby

標準のモジュールだけで完結したかったので WEBrick を使いました。 WEBrick モジュールだけで完結するので、リファレンス等が分かりやすかったです。 ( この辺は大クラス主義のおかげかもしれません。 ) 証明書と中間認証局は別ファイルにしないといけないようです。

#!/usr/bin/env ruby

require 'webrick'
require 'webrick/https'

port = (ENV['PORT'] || 8443).to_i

puts "port=#{port}"

httpd = WEBrick::HTTPServer.new({
  DocumentRoot: './public',
  BindAddress: '0.0.0.0',
  Port: port,
  Logger: WEBrick::Log.new(nil, level=WEBrick::BasicLog::WARN),
  SSLEnable: true,
  SSLCertificate: OpenSSL::X509::Certificate.new(File.open('./certs/cert.pem').read),
  SSLExtraChainCert: [OpenSSL::X509::Certificate.new(File.open('./certs/chain.pem').read)],
  SSLPrivateKey: OpenSSL::PKey::RSA.new(File.open('./certs/privkey.pem').read)
})

Signal.trap('INT') {
  httpd.shutdown
}

httpd.start()

Python

ssl.wrap_socket というのは Python 3.2 以上では非推奨で Python 3.7 からは禁止だそうです。 まずはじめに ssl context というのを作らなくてはいけないのですが、ドキュメントではクライアント側とサーバー側両方の解説があるので初心者にはわかりにくかったです。 しかもサーバー側を作る時に ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) とするのはチョッとした罠です。

あと、SimpleHTTPRequestHandler がカレントディレクトリからコンテンツを配信すること前提のようなので chdir するという汚いサンプルになってしまいました。

#!/usr/bin/env python3

from http import server
import ssl
import os

if __name__ == '__main__':
    address = ('0.0.0.0', 8443)

    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain('certs/fullchain.pem', 'certs/privkey.pem')

    os.chdir('public')
    httpd = server.HTTPServer(address, server.SimpleHTTPRequestHandler)
    httpd.socket = context.wrap_socket(httpd.socket)

    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass

    httpd.server_close()

Node.js

手抜きして express を使ってしましました。そのおかげで少ない行数で済んでいます。

#!/usr/bin/env node

const fs = require('fs')
const express = require('express')
const https = require('https')
const app = express()

const port = Number(process.env.PORT || 8443)

const options = {
  cert: fs.readFileSync('./certs/fullchain.pem'),
  key: fs.readFileSync('./certs/privkey.pem')
}

app.use(express.static('public'))

server = https
  .createServer(options, app)
  .listen(port, () => console.log(`start port=${port}`))

Docker (alpine + lighttpd)

プログラム言語ではありませんが、 Docker (alpine + lighttpd) でもやってみました。 lighttpd では鍵ファイルと証明書ファイルを連結する必要があるので起動時にやっています。

Dockerfile

FROM alpine:3.8

RUN apk --update add lighttpd && rm -rf /var/cache/apk/*

COPY ./public /var/www/localhost/htdocs

RUN cd /etc/lighttpd &&\
    sed -e '/^server.modules = (/a"mod_openssl",' \
        -e '/^# server\.port/aserver.port = 443' \
        -e '/^# ssl.engine/assl.engine = "enable"' \
        -e '/^# ssl.pemfile/assl.pemfile = "/etc/ssl/certs/server.pem"' \
        -e '/^# ssl.pemfile/assl.ca-file = "/etc/ssl/certs/chain.pem"'\
        --in-place \
        lighttpd.conf &&\
        echo -e '#/bin/sh\n'\
                'cat /certs/privkey.pem /certs/cert.pem > /etc/ssl/certs/server.pem\n'\
                'cat /certs/chain.pem >/etc/ssl/certs/chain.pem\n'\
                'chmod 600 /etc/ssl/certs/*.pem\n'\
                'lighttpd -f /etc/lighttpd/lighttpd.conf\n'\
                'sleep 1\n'\
                'tail -f /var/log/lighttpd/error.log -f /var/log/lighttpd/access.log\n' > /start.sh &&\
        chmod +x /start.sh

EXPOSE 443

CMD "/start.sh"

起動

$ docker build . -t <タグ名>
$ docker run --rm -it -p 443:443 -v "$(pwd)/certs:/certs:ro" --name <コンテナ名> <タグ名>

リポジトリ

なお、以上のスクリプトHiroshiOkada/https-example: https 静的サーバーサンプル に置きました。

python3.7 最小限の web サーバー

今年の中頃からぼちぼち python の勉強を始めています。普段は Jupyter Notebook でデータ処理等のスクリプトとかを書いて練習しています。

今日は気分を変えてミニマムな web サーバーってどうやって書くか調べてみました。

公式リファレンスだと https://docs.python.org/ja/3/library/http.server.html あたりなのですが、do_GET() あたりの書き方がわからなくて、おもったより時間がかかりました。(1時間くらい) web には未だ python2.x の情報が溢れているし、python3.x でもバージョンによって思ったより差異があって調べにくかったです。

Hello とだけ返す web サーバ (python3.7)

#!/usr/bin/env python3.7

from http.server import BaseHTTPRequestHandler, HTTPServer

class MyHTTPRequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        self.wfile.write(bytearray('Hello', 'utf-8'))

my_httpserver = HTTPServer(('', 8080), MyHTTPRequestHandler)

try:
    my_httpserver.serve_forever()
except KeyboardInterrupt:
    pass

# ちゃんと閉じないとしばらくアドレスが開放されないので明示的に閉じる
my_httpserver.server_close() 

自宅で実行している Jupyter Notebook を外からアクセスしたかったので、リバースプロキシを設定しました

Jupyter Nodebook が便利なので、最近は自宅のデスクトップで立ち上げっぱなしにして、外出先からは ssh protfowarding で接続していました。

けれども、ブラウザだけでアクセスできたほうが便利な事も多いので、nginx の reverse proxy の経由でアクセスできるように設定をしました。

JupyterHub という複数の Jupyter Notebook を reverse proxy 経由で配信する設定の文書は以下にありました。

Using a reverse proxy — JupyterHub 0.9.4 documentation

単一の Jupyter Notebook Server でも大きな違いはないと思いますので、それを参考に僕の環境で必要だった調整加えた設定を以下に記述します。

(以下、 jupXXXX.toycode.com は仮の名前です、実際には少し違う名前で運用しています。)

自宅のネットワーク構成

f:id:OkadaHiroshi:20181010141822p:plain

  • デスクトップ PC (192.168.100.100) の 8888番ポートで Jupyter Notebook を実行しています。
  • 有線ルータの簡易DNS機能で自宅ネットワークの内部では jupXXXX.toycode.com を名前解決すると 192.168.100.200 (HP ProLiant) を返すようになっています。
  • 外部のインターネット上では、*.toycode.comは自宅のグローバルIPに結び付けられているので、jupXXXX.toycode.com を名前解決すると自宅のグローバルIPアドレスが返ってきます。
  • 有線ルータの静的NAT機能で外から来た 443 (https) ポートは Ubuntu 16.04 Server がインストールされている 192.168.100.200 (HP ProLiant) の 443 (https) ポートに届くようになっています。

192.168.100.200 (HP ProLiant) 上で実行している nginx の Dockerfile

nginx は Docker コンテナ上で実行しています。SNI (名前ベースのバーチャルホスト) になっています。

Dockerfile はこんな感じです。

FROM nginx:mainline-alpine

RUN apk update -q && apk add openssl
RUN openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096

RUN rm /etc/nginx/conf.d/*

COPY nginx.conf server.crt server.key /etc/nginx/
COPY conf.d/ /etc/nginx/conf.d/

EXPOSE 80 443

nginx.conf

 user  nginx;
 worker_processes  1;

 error_log  /var/log/nginx/error.log warn;
 pid        /var/run/nginx.pid;


 events {
     worker_connections  1024;
 }


 http {
     include       /etc/nginx/mime.types;
     default_type  application/octet-stream;

     log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                       '$status $body_bytes_sent "$http_referer" '
                       '"$http_user_agent" "$http_x_forwarded_for"';

     access_log  /var/log/nginx/access.log  main;

     sendfile    on;

     keepalive_timeout  65;

     gzip  on;

     server {
         listen 80 default_server;
         server_name  _;
         return 444;
     }

     server {
       listen 443 ssl default_server;
       server_name  _;
       ssl_certificate      /etc/nginx/server.crt;
       ssl_certificate_key  /etc/nginx/server.key;
       return 444;
     }

+    map $http_upgrade $connection_upgrade {
+        default upgrade;
+        ''      close;
+    }
+
+    ssl_session_timeout 5m;
+    ssl_session_cache shared:SSL:1m;
+
     include /etc/nginx/conf.d/*.conf;
 }
  • +が元の nginx:mainline-alpine の nginx.conf に追記した部分です。
  • Using a reverse proxy — JupyterHub 0.9.4 documentation に記述してあった内容を一部 http セクションに移動しました。
  • 特に ssl_session_cache shared:SSL: はバーチャルホスト間で共用するので server セクションではなくてここに書くべきだと思います。
  • 有効期間等は、自分一人で使うので負荷も大したことはないし、間違えた場合にキャッシュ等が長時間残ってしまうのを避けるため短めに設定しまました。

conf.d/jupXXXX.conf

server {
    listen 80;
    server_name jupXXXX.toycode.com;

    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;

    server_name jupXXXX.toycode.com;

    charset UTF-8;

    ssl_certificate /etc/nginx/server.crt;
    ssl_certificate_key /etc/nginx/server.key;

    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';

    add_header Strict-Transport-Security max-age=3600;

    location / {
        proxy_http_version 1.1;

        proxy_pass http://192.168.100.100:8888;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 300;

        # websocket headers
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }

}
  • return 301 https://$server_name$request_uri; http は恒久的に使う予定がないので 302 ではなくて 301 にしました。
  • listen 443 ssl; nginx公式サイトのngx_http_ssl_module によると ssl on; という書き方は obsolete だそうです。
  • ssl_protocols TLSv1.2; 僕の使っているブラウザは全て TLSv1.2 をサポートしているので TLSv1.1 等は記述しませんでした。
  • proxy_http_version 1.1; 必要なので追加しました。
  • ssl_stapling の設定は resolver の設定が必要で、僕の場合は LAN 内では外側とは別なIPアドレスを返しているので削除しました。
  • ここでも有効期間等を短めに設定しています。
  • ssl の証明書は以前の記事 で取得したワイルドカード証明書を利用しています。

結論

いまの所、これで上手く動いています。正直いって暗号のアルゴリズム ssl_ciphers についてはコピペしただけので自信がありません。

(SSL Server Test は一応 A になりました。)