dockerリバースプロキシ環境

前記事で書いたように従来のApache仮想ホストと同じことをdockerブリッジネットワークで構成するためには、リクエストホスト名によって接続コンテナを切り替える機能が必要とのこと。しっかり勉強すれば自力でも出来るかも知れないが、先人の作り上げた構成をお手軽に実現出来るのがdockerの良いところということで、ネット検索でも評判の良いjwilder/nginx-proxyを使用することにしました。

version: '3'
services:
  nginx_root:
    build: ./rootimg
    image: jwilder/nginx-proxy:certbot
    container_name: nginx_root_container
    hostname: nginx_host
    depends_on:
      - http_default
    privileged: true
    environment:
      - DEFAULT_HOST=base.marochanet.org
      - TZ=Asia/Tokyo
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      default:
        ipv4_address: 192.168.29.2
    ports:
      - "80:80"
      - "443:443"
    restart: always
  http_default:
    container_name: http_default_container
    image: nginx
    hostname: http_default_host
    expose:
      - "80"
    environment:
      - VIRTUAL_HOST=base.marochanet.org
      - TZ=Asia/Tokyo
    volumes:
      - ./html:/usr/share/nginx/html
    restart: always

networks:
  default:
    driver: bridge
    name: shared
    ipam:
      config:
        - subnet: 192.168.xx.0/24
          gateway: 192.168.xx.1
          ip_range: 192.168.xx.16/28

nginx_rootというサービスがjwilder/nginx-proxyを使用したリバースプロキシコンテナです。build:でイメージを作り直していますが、Letsencryptの証明書を取得/更新するためにcertbotをインストールしているだけです。

  • rootimg/Dockerfile
FROM jwilder/nginx-proxy

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install --yes --no-install-recommends \
      certbot \
    && rm -rf /var/lib/apt/lists/*

2個目のサービスhttp_defaultはとりあえずのWEBサーバー。テスト用にHTML返すだけなので特別なことはしていませんが、これだけでOKです。キモはenvironment節の環境変数として記されたVIRTUAL_HOSTで、ここに設定したURL向けのリクエストがこのコンテナに向けられるようにリバースプロキシが上手い具合に設定してくれます。

プロキシサービスのコンテナにある環境変数DEFAULT_HOSTはそのまんまの意味で、行き先の見つからない宛先(IPアドレス直とか)の場合の飛び先で、この場合宛先がhttp_defaultサービスと同じになっているので、迷い込んで来た人?IPアドレスで飛んできたスキャンな人へのメッセージページ/エラーページにすると良いかもしれません。

一番最後のnetworks:ではブリッジネットワークの設定をしています。name:節の shared がネットワーク名になります。サブネット、ゲートウェイが設定出来、さらに ip_rangeは、よく理解出来てませんが、固定IPを割り当てる範囲を決めているようでした。固定IPは 1~15で、DHCP的なアドレス割当ては16以降になっているようです。

基本的にはこの共通ネットワークに入ってさえいれば同じdocker-composeで作成していないサービスでもリバースプロキシの傘下に入ることが出来ます。別プロジェクトのdocker-composeでは、外部参照のネットワークとしてsharedを定義しておきます。

networks:
  default:
    external:
      name: shared

当サイトの他のサービス(djangoサイト、wordpress、rainloop WEBメール、nextcloud)は全てこの方法でリバースプロキシ傘下に入っています。

certbotも入れたので、次はletsencryptでの認証関連の設定となりますが、今回は割愛しています。jwilder/nginx-proxyではお手軽な方法が用意されているので、letsencrypt用の起動スクリプトも含めて次回のメモ記事で紹介します。

docker環境も整って勉強や実験も一段落してきたので、そろそろブログネタの方向性も(趣味的な方向へ)変えていきたいなと思うのですが、コロナとか自粛モードでなかなかネタ集めが出来ないですねー(言い訳
・・・・まぁこのへんのネタもじゅーーーぶんに趣味的ではあるけど。

docker対応ufw(iptabels)設定の件

ここでdockerについての解説をするのはおこがましいので、下記に覚え書きリンクを貼付ける

ローカルLAN環境でWEBサーバーを運用するだけなら良いが、当サイトにdockerを導入するに当たって一番の懸念事項がファイアウォールとの相性問題があった。

当サイトと同じUbuntu20.04LTSで下記のようなファイアウォール設定であったとして

xxxx@user$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
80/tcp (v6)                ALLOW       Anywhere (v6)
443/tcp (v6)               ALLOW       Anywhere (v6)

dockerコンテナで8080ポートでwebサイトを立ち上げると、ファイアウォールをするりと抜けて外部に公開できてしまう(下記テキトウdocker-compose.yml)

web:
  image: nginx
  volumes:
   - ./templates:/etc/nginx/templates
  ports:
   - "8080:80"
  environment:
   - NGINX_HOST=foobar.com
   - NGINX_PORT=80

当サイトではufwファイアウォールの運用で特定IPアドレスからのアクセスを遮断している(メールサーバへの不正アクセスやwordpressへの不正ログイン試行を手動・自動で監視して遮断対象アドレスをリストアップ)。

ufwコマンドで設定するポートアクセス許可よりも前で設定するために、/etc/ufw/before.rules に記載のあるチェインufw-before-inputにログ出力と遮断コマンドを追加している。

# blacklist-ipset-S ここにブロック対象のアドレスを追加
-A ufw-before-input -s xxx.xxx.xxx.xxx -j auto-blocklist
# blacklist-ipset-S

# タグをつけたログを残してドロップさせるチェイン
# ファイルの先頭の*filter節に追加する必要あり?
-A auto-blocklist -j LOG --log-prefix "[BLOCKLIST]"
-A auto-blocklist -j DROP

こうすることでufw起動時にiptableの遮断リストが設定される。

しかし、上記のdockerコンテナはこの設定も効かず、遮断対象アドレスも通過してしまう。

そこで、まずdockerコンテナが何故ufwの設定を無視できるのかを調べてみる。(iptableコマンド)

user@server:~$ sudo iptables -nvL
Chain INPUT (policy DROP 2979 packets, 152K bytes)
 pkts bytes target     prot opt in     out     source               destination
3687K 3750M ufw-before-logging-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3687K 3750M ufw-before-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
83453 4149K ufw-after-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-after-logging-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-reject-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0
77337 3836K ufw-track-input  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain FORWARD (policy DROP 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
4406K 5775M DOCKER-USER  all  --  *      *       0.0.0.0/0            0.0.0.0/0
4406K 5775M DOCKER-ISOLATION-STAGE-1  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3634K 4639M ACCEPT     all  --  *      br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
80854 4814K DOCKER     all  --  *      br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
 675K 1102M ACCEPT     all  --  br-a52db8fb2804 !br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
70296 4218K ACCEPT     all  --  br-a52db8fb2804 br-a52db8fb2804  0.0.0.0/0            0.0.0.0/0
71973  318M ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
54110 3031K ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ufw-before-logging-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-before-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-after-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-after-logging-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-reject-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0
    0     0 ufw-track-forward  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 4 packets, 196 bytes)
 pkts bytes target     prot opt in     out     source               destination
3574K 2258M ufw-before-logging-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3574K 2258M ufw-before-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-after-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-after-logging-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-reject-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0
27893 2070K ufw-track-output  all  --  *      *       0.0.0.0/0            0.0.0.0/0

この辺りもこのために改めて勉強したため、理解不足かも知れないが、、、

普通にufwで設定するファイアウォールはINPUTチェインが主で、5行目のufw-before-inputのチェインから繋がっている(当サイトのアクセス遮断リストもここに繋げている)。それに対してdockerのファイアウォールはFORWARDチェインであり、16,20行目のDOCKERチェインでコンテナで設定したポートの待ち受けを実現している。それはufwのFORWARD設定よりも優先しているので、アクセス制限はそれよりも前で行う必要がある。ここでアレ?と思うのがDOCKERチェインよりも前にあるDOCKER-USERチェイン。このチェインの中身は空白で何もせずに帰ってくる仕組み。

ならば、ここに遮断リストを追加してやれば良いんじゃね?という感じで、

$ sudo iptables -A DOCKER-USER -s xxx.xxx.xxx.xxx -j DROP

としてみたら、無事遮断していることを確認(とりあえずスマホのIPアドレスを遮断してみて確認)。

実は英文の方のdocker docsにはそれっぽいことが書いてあった。。

次に問題はこのリストを起動時に読み込ませるにはだけど、これは何のことはなくufwの助けが借りられることが分かった。先の遮断リストをログ出力しつつDROPするチェインを追加した要領でDOCKER-USERチェインも/etc/ufw/before.rulesに記載してしまおうという思いつきで実験、、上手く動作したので下記に示します。

$ sudo cat /etc/ufw/before.rules
# Don't delete these required lines, otherwise there will be errors
# added for docker access limits. 21/08/17 by mamiyan
*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]
:DOCKER-USER - [0:0]           # <---- これ追加
:auto-blocklist - [0:0]        # <---- ログ出力&DROP処理用チェイン
:docker-blocklist - [0:0]      # <---- 同上
# End required lines

・・・・・
# for blocklist custon chain 追加チェイン
-A auto-blocklist -j LOG --log-prefix "[DROPLIST]"
-A auto-blocklist -j DROP

# for docker custom chain 追加チェイン
-A docker-blocklist -j LOG --log-prefix "[DROPDOCKER]"
-A docker-blocklist -j DROP

# blacklist-ipset-S ここから
-A ufw-before-input -s xxx.xxx.xxx.xxx -j auto-blocklist
-A DOCKER-USER -s xxx.xxx.xxx.xxx -j docker-blocklist
# blacklist-ipset-E ここまでにリスト追記

24行25行のように二つのチェイン向けに遮断アドレスリストを追記して、$ sudo ufw reload でdocker向けのファイアウォールが設定されます。その他のアクセス制限もこの/etc/ufw/before.rules の DOCKER-USERチェインに記載しておけば良いと思います。

この辺りは、ufwを少し触れる程度の知識からiptableの仕組みとチェインを触れる程度までじっくり勉強が出来て良かったと思います。またここでCentOSとかのRHL系でfirewalldとかになると変ってくるのだろうか。そもそもあちら側は相性問題は無いのかな。

docker環境への移行

約一ヶ月ぶりの記事になりますが、夏休みということで。

この夏休みは、雨続きやら新型コロナの蔓延防止やら緊急事態やらの巣ごもりで、夏バテと闘いながら、従来から引続きpython(django)、javascript(Three.js)勉強行いながら、前回の記事でチラッと書いたdockerの勉強・実装実験を行っておりました。もともろvmwareやら仮想環境に興味があったので、比較的敷居が低かった感じで、いろいろ試しているウチに勢い余って、当サイトのWEB環境もApache2の仮想ホスト環境から、dockerコンテナのブリッジネットワーク構成に移行させてしまったというお話。さらにはメールサーバーもdockerコンテナ化・・・8月中に一通り移行を終えたところで一息ついている感じです。

とっかかりは楽しくて次々とdocker化が進んでいったが、やっぱり全く同じ環境を再現しようとすると色々と問題が出てきたり、ネット調査だけでは答えの出ない問題に突き当たったりとネタだけは貯えたので、覚え書きレベルですが追々と記事にして置こうかと思います。

つい今の今も、この記事を書こうとwordpress(これもdockerコンテナ化済み)の記事編集をしていたところ、メディアファイルがアップロード出来ない問題が発生(移行時に試していないのがいけない・・・)。半日がかりの原因究明でしたが、結局はアップロードパスの修正忘れという単純なポカミス。。まぁdockerという新しい環境への移行なので、まずそちら方面を疑ってしまい、思わぬ深みにはまってしまうというよくある罠でした。

今回の移行ですが、従来構成を下記に示す。普通のubuntuのままなのでapache単独サーバーでvhost.confの設定で仮想ホストを構成していた。

従来構成

同様の環境をdockerリバースプロキシ環境に移行した構成を下記に示します。

移行後のdocker構成

dockerブリッジネットワークでほぼ全てのサービスをコンテナ化してリバースプロキシサーバにて80番ポートと443番ポートを受けて文字通りの仮想ホスト構成としている。メールサーバーは受信のために25番ポートを空けているが、こちらは図のように直接外に向けてポートを空けてしまうと、ホスト内のサービスメール(cronやlogwatchなど)が動作しなくなってしまうので、実際はホスト側でもpostfixを動作させホスト内メールの面倒を見ながら外部から来たメールはコンテナのメールサーバーに転送する仕組みとしている。

一ヶ月の試行錯誤の結果ほぼ従来通りの動作を確認できたが、なんか妙に重いかも知れない。。。

久々のブログ記事でいきなりファイルアップロード不可とか、メッチャ焦りました。新環境では十分なテストが必要ですね。メールサーバーのdocker化もテキトウに確認して運用始めては→問題発覚→環境戻し→デバッグやり直し-の繰返しでした。たいていはショウモナイポカミスなんですが、そういうのに限って見つけるのに半日から1日作業となるのが定期。

Three.jsによる3Dオブジェクト表示

Three.js +Orbitcontrol.js + GLTFLoader.js を使用。モデルはFFXImodelviewer2を使用してmqo形式で取り出してBlenderにて明るさ調整をしつつglbで出力、WEBアプリにて表示している

FF11をやっていた頃の自キャラっぽいモデル。サンプル用だけど一応コピーライト表示しておきますが、不味かったら消します。

少し時間が空いてしまったが、自サイト構築としてdjangoによるポータルの仕組みがある程度出来てきたところで、次はフロント側の勉強をしておりました。

フロント側はjavascriptになるため、ローカル環境で試しながら少しずつ勉強をしてきた。

今回はポータル側のdjangoアプリから埋込みサービスを作成して、こちらのwordpressで表示するテストを行った。

iframeタグにてポータル側のURLを指定してjavascriptのThress.jsライブラリを用いてWEBGL用フォーマットglTFの3Dオブジェクトを表示させている。

wordpressの方をもう少し勉強すれば埋め込みを使用せず、直接javascriptのプラグインを作ってしまえるのだが、それはまたいずれということで、まずはここまで勉強した成果を残すことを優先しました。

いずれ3Dオブジェクトをリアルタイムに操作してサーバー側とのやりとりをしながらのアプリ作成を目論んでこういう仕組みとしてみたということで。。。

このキャラです。同じ帽子を被せたかったのですが、モデルに帽子を被せるとポニーテールが飛び出してしまったので今回は無しで。。。

Windows Terminal

しばらく最新の動向とか触れる機会が少なかったので、最近のツール事情にはいまだに驚かされたりする。当方では趣味でさくらVPSに契約して個人用のサイトを運営をしており、当たり前だが自宅のローカル環境から操作している。今までは通信ソフトのPuttyでサーバーにSSH接続して、サーバー上でemacsなどのエディタで構築作業をおこなっていたが、前回の投稿で書いたようにマイクロソフトのVScodeエディタでSSH接続することでローカル同じ環境で編集作業を行なうことが出来る。

それでも実際ターミナル操作をするときはPuttyを使用していたのだが、今度はWindows Terminalというものを知ることとなる。こちらはWindowsのストアアプリで、コマンドプロンプトからPowershell、WSLも対応しており、プロファイルを追加することでssh接続も可能となる優れもの。

しかもこちらはWindows版のOpenSSHのサービスで動いているので、SSH-Agentサービス(Windowsサービスで自動実行)ログイン時にパスフレーズを入れる必要もないので便利。

ローカル作業時も任意のフォルダで右クリックすると「Windowsターミナルで開く」のメニューが追加されており、Powershellが起動するようになる。(これは、以前よりシフトを押しながら右クリックでPowershellで開くことが可能だったが)

ということで、これも何かあったときのための覚え書き。

VScodeの件もそうだけど、今までは何だったのか?!ってくらい超便利な感じで、きっとまだ触っていないDockerやなんかも同じように感動するんだろうな、、、、と思いつつも、なんだかね。こんなすごい複雑なシステムの開発環境を使って、作っているものと言えば、趣味のオモチャ程度・・・。超豪華な財布の中に2000円くらいしかい入ってない感じw
精進せねばと思うこのごろ。。。

Bootstrap5対応その他

という記事を書こうとWorpressダッシュボードに移動したら、いつのまにかWordpressが5.8にバージョンアップしていた。例によってDBバックアップを取れとの忠告も聞かずに、速攻アップデートボタンポチり。

特に変った様子もないが、変化点は追々チェックするとしよう。

でBootsrtap5対応だが、先月辺りまでのこのサイトの引越作業の時点でベータ版が存在していたが、あまり気にしていなかったところ、ポータルサイト構築を兼ねたWEBアプリ開発の勉強がてら色々調べていたところバージョン5のリリースを知った(今ごろですが)。機能的にはあまり差は無い(というかよく読んでいない)がJQuery依存の削除やIEなど古いブラウザサポート終了など良い感じのアップデートのようなので、まずはdjangoテストサーバー環境で導入実験を行ない、ある程度問題ないことを確認したところで主ブランチに統合して本番環境のポータルサイトでも採用することとした。

見た目はあまり変らないが、テーマのデフォルトの文字サイズなどが異なるため、若干文字が小さかったり、メニューの文字が太字だったりと細かな気にならない違いが見られた。

4.5から5への対応に関する変更点であるが、前回までのSass導入でHTML(テンプレート)にBootstrapのクラスタグを直接書くことは少なくなったので、基本的には階層構造のスタイル定義をしているSassソースファイルを修正することで対応した。

主な違いは、marginやpaddingを表現するクラスで、たとえば右マージンはmr、左マージンはmlというようにrightとleftの頭文字で表現していたが、バージョン5より、左がstart、右がendと表現するようになりそれぞれマージンとしてはms、meと表記するようになった。これはおそらく多言語対応でRTL(右から書く文化)にも対応する事により水平方向の表記は右からと左からというより、始めと終わりという意味で統一したのだろう。

あとハマったのはアコーディオン型のメニューアニメーションの件で、若干名称が異なり、

  • data-toggle → data-bs-toggle
  • data-target → data-bs-target

になっていた。当サイトはこの位しか機能を使っていないので、以上の修正で無事以前と同様に動作するようになった。

というか、せっかくBootstrap5を導入したのに、新機能を調べて(あるのか?)導入実験とかしたいのだが、他にもいろいろやりたいことありで、なかなか優先順位に困っているところ・・・
思いつき順ということもありますが・・・

VScodeの覚書

サーバー作業の関係もすっかりローカルのVScodeからリモートSSHで行う環境に慣れきってしまい、徐々にputtyとかの普通のSSH端末ソフトを使わなくなってきた。しかし、たまーにROOTで作業をしたいとかApache Userでコンテンツを直接修正したい時などはputtyなどを使用していたが、これもいっそのことVScodeをSSH端末代わりに使用してしまってはどうだろうか?との思いになり、しばらく使ってみたらいきなり躓いた。

VScodeではemacsキーバインドのプラグイン(Awesome Emacs Keymap)を使用しているが、VScodeのリモートSSHでコマンドラインを使用して、他のユーザーにsuしてエディタを使用する場合、パーミッションの関係でVScodeは使用できないのでリモートのemacs系エディタを使うことになる。そこで編集を終了して「いざエディタ終了」と「Ctrl+X、Ctrl+C」と打つと、当のリモートのemacsだけでなく、VScodeもこのキー操作を拾ってしまい、VScodeも終了してしまうという問題が起こった。

考えた挙句、しばらくは「M-x kill-emacs」と手動で打って終わらせることは可能だったが、これではどう考えても不便。。。

というか、よく考えたらキーバインドを弄って「 Ctrl+X、Ctrl+C 」を解除すれば良いじゃない?ということで、探してみてもキーマップのファイルが見つからない。

調べた結果、Windows版だと左下の歯車マークの設定から、「キーボードショートカット」を選ぶとよいということで、「Close Window」に割り当てられた「 Ctrl+X、Ctrl+C 」を解除することで、リモートemacsでもストレスなく使用できるようになった。

・・・という覚書でした。

BootstrapとかCSSの事からSASSにたどり着くまで

djangoを通じてWEBアプリの勉強中。ポータルサイトのdjangoアプリで一通り遊び尽くしたところで、今度はフロント側の見た目やスクリプトに手を付け始めた。

今まではサイトの情報をそのままコピペするようにサイトを作ってきたので、その辺のメカニズムについて勉強を始めた所。今までHTMLとかCSSは必要に応じて編集してきたが、知識は初期のHTML3?あたりで止っているので、そのあたりも昨今のHTML5まで追いつけたら良いなぁ位には考えている。

BootstrapでレスポンシブルなWEBサイトを・・・と分かったような分かってないような事を書いてみたが、使っていくと結構便利で、あんまり考えなくても一応ちゃんとしたサイトが作ることが出来る(本サイトのポータルもdjango+Bootstrap)。しかし、いろんなサイトの情報をつまみ食いしながらゴチャゴチャやってきたら、非常にごちゃごちゃのHTMLソースが出来上がってしまった。

例えば、下記カード型サイドメニューの一部だが、クラスタグのカタマリのようになってしまう。

    <div class="container-fluid">
      <div class="row justify-content-md-center">
        <div class="col-md-2 px-1">
          <!-- 右側サイドバー -->
          <div class="card px-0 mx-0 my-2">
            <div class="card-body">
              <h4 class="card-title btn rounded-pill btn-outline-dark">Side Menu</h4>
                <ul class="list-group list-group-flush">
                   <li class="list-group-item">
   ・・・・・・

そもそも、HTMLとCSSに分かれたのって、文書と見た目の装飾を分けるのが目的で、HTMLの新しくなるにつれて、ボールドとかイタリックとか文字サイズの直接変更みたいなのは削除されてきたように思うのだが、昨日今日勉強し始めた素人的な感想だが、一応昔からHTMLには接してきてその進化も遠巻きに見てきたので、HTMLはあくまでも文書とその構造を定義するもので、見た目や表現はCSSの仕事という認識。

Bootstrapは確かに便利でいろんなパーツが揃っているが、こうもクラスタグで文書の見た目についての情報をベタベタ貼付けていると、何か本末転倒のような気がするのもあるかなーと思う。

確かに汎用性があるのは良いのだが、もっとCSSの方でスッキリ纏められないものかと、調べてみると、世の中考える人は多いもので、SASSというツールに辿りついた。これはCSSのプリプロセッサのようなもので、CSSだけでは手が届かなかった構造化したスタイルシートを定義して、CSSにコンパイルするツール。

これを使えば例えば 「card-bodyクラスのブロックに含まれるh4ヘッダはこのスタイル」と決められる。またdivを使用せずに「cardクラスのブロックに含まれるsectionというブロックはcard-bodyのスタイルを持つ」みたいな感じでdivを多用せずに意味のあるブロックで文書を構成することが出来る。

.cardclass {
  @extend .card, .px-0, .mx-0, .my-2;
  section {
    @extend .card-body;
    h4 {
      @extend .card-title, .btn, .rounded-pill, .btn-outline-dark;
    }
    ul {
      @extend .list-group, .list-group-flush;
      li {
        @extend .list-group-item;
      }
    }
  }
}

と定義出来、上のHTMLも

    <div class="container-fluid">
      <div class="row justify-content-md-center">
        <div class="col-md-2 px-1">
          <!-- 右側サイドバー -->
          <div class="cardclass">
            <section>
              <h4>Side Menu</h4>
                <ul>
                  <li>
   ・・・・・・

のように長々としたクラスタグを書かずにスッキリさせることが出来る。

久々にこの手の勉強を始めて間もないので、こういう考え方で良いのかどうか分からないが、少なくとも文書の構造を定義するHTMLの考え方はこうじゃないかなと、思う所。あとCSSファイルのサイズの肥大化でページの読み込み速度の話もあるかも知れないが、とりあえずは今はそっちは考えないとする。

と、まぁそんな感じで、自サイトを使用して色々試しているが、今日もブラウザキャッシュの罠にハマって半日ほど無駄にしてしまった。。テスト環境と本番環境で同じファイルなのに本番環境のみ一部のクラスタグの部分が反映されないとか・・・これもキャッシュだったとは。とりあえず難しいことは考えずにこまめにキャッシュクリアが大事です。
本番環境にはSassコンパイラ入れてなかったので、いろいろ大変なことになっていた。

不正アクセスログ集計ツール⑥

最近たまに書くというとこればかりの話になっているが、その割には一向に進まない・・・

djangoへの実装だが、今回はデータベースを使用するわけでもないテストページなので新たにアプリケーションを追加することなく、ポータルのトップページにコッソリぶら下げることとする。(とりあえずログインユーザーのみのアクセス制限は掛ける予定として)

まずはurls.pyへのページ追加

app_name = 'top'

urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('iplog/', views.BadIPView.as_view(), name='badip'),
    path('ipsum/', views.IpSumView.as_view(), name='iplog'),
]

indexは通常のポータルページで、今回 iplogとbadipという名前のページを追加する。iplogが不正アクセスの疑いのある(何回もログイン失敗を繰り返す輩)のIPアドレスとアクセス回数のリスト、badipがそのIPアドレスのアクセス履歴を表示するページ。

どちらのクラスもデータベースを使用せず、サーバー側のデータをリスト化して参照するので、まずはIpSumViewクラスから。

class IpSumView(generic.ListView):
    template_name = 'top/ipsumlist.html'
    context_object_name = 'ipaddress_records'
    queryset = None
    paginate_by_default = 20
    form1 = None
    form_initial = {}

template_nameは使用するテンプレートのパス名、context_object_nameはテンプレートで使用するリスト。querysetは通常であればデータベースの問い合わせとして、モデルオブジェクトとの接続を行なうが、ここではNoneとしてメソッドのget_queryset()をオーバーライドして先のアクセス解析関数からの戻り値のリストとして返す

    def get_queryset(self):
        """URL引数を取り出すサンプルとデータベースの代わりに動的にリストを作成する
        """
        days = int(self.request.GET.get('days', 1))
        max = int(self.request.GET.get('count', 1))
        srt = self.request.GET.get('srt', 'count')
        src = self.request.GET.get('src', 'bt')
        return get_login_report(get_datetime_hours_ago(days * 24), get_datetime_hours_ago(0), max, src, srt)

実際はフォームのためのパラメータセットなどがあるが省略。
リクエストGETでパラメータを設定するが、パラメータがない場合の初期値を取るため上記のような書き方となっている。get_login_reportがアクセス集計関数

def get_login_report(st, ed, max = 1, src = 'bt', srt = 'count'):
    iplist = {}
    #hists = get_login_history(st, ed)
    list1 = get_django_loginfails(None, st, ed) if src != 'wp' else []
    list2 = get_wp_loginfails(st, ed) if src != 'dj' else []
    hists = list1 + list2
    blist = get_blocklist()
    for adr in map(lambda x: x['address'], hists):
        iplist[adr] = iplist[adr] + 1 if adr in iplist.keys() else 1
    retv = [{'address':adr, 'count': cnt} for adr, cnt in iplist.items() if cnt >= max and adr not in blist]
    if srt == 'count':
        retv.sort(key = lambda x: x['count'], reverse=True)
    else:
        retv.sort(key = lambda x: hash(x['address']), reverse=True)
    return retv

wordpressのログイン履歴データベースからのリストと、djangoポータルページへの履歴を合せて、アクセス回数ごとのリストに変換。指定回数以上アクセスした対象を返す関数となっている。

左がとりあえずの完成形となる。BootstrapのNavibar以外は装飾無しの素のHTMLなので飾りっ気一切無し。この辺の見栄え関連は次回以降勉強していくことにする。

めっちゃくちゃ端折った感じで完成してますが、所詮覚え書きなので、ここまでの作り込みに関して気が付いたときに勘所とかを追記していこうと思います。

不正アクセスログ集計ツール⑤

いよいよ集計アプリをdjango WEBアプリに実装。その前に、WordpressのLoginHistoryプラグインのログも拝借するコードを追加

from datetime import datetime, timezone, timedelta, tzinfo
import MySQLdb

tz_jst = timezone(timedelta(hours=9), name='JST')

def get_wp_loginfails(st, ed, address=None):
    """
    wordpressのデータベースから認証履歴のレポートを返す(要:プラグイン User Login History)
    [引数] st : レポート対象の開始日時
        ed : レポート対象の終了日時
        address : レポート対象のIPアドレスオブジェクト(Noneの場合は全て)サブネット検索対応
    """
    user = '<USER>'
    passwd = '<passwork>'
    host = 'localhost'
    db='<dbname>'
    rows = None
    with MySQLdb.connect(user = user, passwd = passwd, host = host, db = db) as conn:
        with conn.cursor() as cur:
            (stat1, ed1) = (x.astimezone(timezone.utc).replace(tzinfo=None) for x in (st, ed))
            sql = 'select username, time_login, ip_address, login_status from wp_fa_user_logins'\
                ' where time_login > %s and time_login < %s' #and login_status = %s'
            cur.execute(sql,(stat1, ed1)) # <> 'login'
            rows = cur.fetchall()
    # rows 集計
    rows = rows if address is None else [row for row in rows if ipv4adrset(row[2]) == address]
    retlist = [
        {
            'name':row[0],
            'datetime':row[1].replace(tzinfo=timezone.utc).astimezone(tz_jst),
            'address':ipv4adrset(row[2]),
            'status':False if row[3]=='fail' else True,
            'source':'wordpress',
        } for row in rows]
    return retlist

なんやらいろいろゴチャゴチャやってるけど、とりあえずmysqlに貯め込んでいるログイン情報を指定期間分取り出してくる。