WEBアプリテンプレート(管理機能)

前回紹介したdjangoWEBアプリテンプレートに関して、個別の機能について気が向いたらメモを残しておきます。

ユーザー(www-data)の設定

djahgoのサービスはwww-dataというユーザー(debian-slimイメージに最初から居る)で起動するので、/code以下のファイル所有権もwww-dataとし、また実際のアプリ開発のために起動したコンテナにVScodeにアタッチして編集したりgitでソース管理出来るようにwww-dataユーザーでログインすることとし、念のためsudo権限を与えておく事とした。

# Dockerfile
RUN apt update; apt install -y git sudo
......
# usermod for www-data & sudo group
RUN usermod -d /code www-data; \
    usermod -s /bin/bash www-data; \
    echo "%www-data ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/www-data

# docker-compose.yml
services:
  django:
    build: .
    image: django_image:test
    user: www-data
    command: /code/startup
.......

こうすることで/code/startupはwww-dataの権限で実行されてテンプレートの構築が行われる。ただし、コンテナでマウントしたディレクトリの扱いがMacとそれ以外では異なるようで、Linuxなどの場合コンテナとホストのUIDが一致していない場合、ホストのユーザーで作成した/codeにwww-data権限で書込が出来ない問題がある。ここでは/code/startupの初期の段階で/codeの所有権をコンテナのwww-dataに取得させている(その為にパスワード無しのsudo権限を与えてしまった・・・・)

adminユーザーの設定

コンテナ側のユーザー管理とは別に、django WEBアプリにも認証があるため、少なくとも管理ユーザーの作成が必要(認証する気がなければ無くても良いかも知れない)。これもサイト構築毎に毎回は面倒なので、/code/startupのテンプレート作成時に自動生成させている。

ユーザーデータベースの更新は本来インタラクティブモードで動作するが、全自動で行うために下記のようにshellでコマンドを食わせている。

# startup
python manage.py migrate # データベースの初期化

cat <<CREATESUPER | sed -r "s/^\s*//g" | python manage.py shell
    from django.contrib.auth import get_user_model
    User = get_user_model()
    User.objects.create_superuser('admin' ,'', 'admin')
CREATESUPER

本テンプレートでは認証画面への遷移や管理画面へのリンクはnavbarに配置しており、テンプレートのcommon.tsでDOM構築している。

テンプレートのDOM構造は先日投稿のものをTypescript対応にしたもので、少し冗長になる感じだけど上手く動いています。これも気が向いたらメモを残しておこうかと・・・・

WEBアプリテンプレート(django+javascript)

code
code
nginx
nginx
Dockerfile
Dockerfile
docker-compose.yml
docker-compose.yml
conf.d
conf.d
uwsgi_params
uwsgi_params
project.conf
project.conf
project.conf
project.conf
/
/
[PROJECT]
[PROJECT]
docker
マウントポイント
docker…
サイト構築
ポイント
サイト構築 ポイント
プロジェクト構成
プロジェクト構成
Text is not SVG – cannot display
# 通常起動
$docker-compose up -d
# 再構築
CLEARSITE=1 docker-compose up -d
テンプレート表示状態

WEBアプリの勉強を始めた所、djangoを用いて同じ構成のサイトを何度も構築するのが面倒でテンプレートを作ることにした。しかしコレばっかりに夢中で肝心の勉強の方がおろそかになってきているのは、いつものお約束。

テンプレート構成

  • docker image 仕様(Dockerfile)
    • debianベースのpythonスリムイメージをベースとする
    • pythonライブラリインストールステージ:dbアクセス用ライブラリの為に一時的に開発ツール一式をインストール(後にスリム化処理)
    • nodejsは一旦aptパッケージインストールした後、npmで最新のバージョンをインストール
    • npmでtypescript、sass、bootstrapをインストール
    • その他チョットしたツール:gitやwget、アプリ実行用ユーザー(user)の作成とsudo権限設定
    • 実行コマンドは/code/startupシェルスクリプト:イメージにはダミーのシェルスクリプトを格納しているが、実際はサイト構築テンプレートとwusgi起動を含むシェルスクリプトを/code/startupとして作成しマウントして実行(後述)。
  • 起動:docker-compose構成
    • djangoは/codeをマウントしてサービスを実行
    • 環境変数CLEARSITE=1とすることでサイトを削除してテンプレートで再構築する(危険コマンド)
    • 構築用のstartupスクリプトが長いため、djangoサービスが始める前にnginxからのサービス要求がありロックすることがあったので、ヘルスチェックを行うようにしている(uwsgiのプロセスチェック)。
  • startupスクリプト(テンプレート構築&サービス起動)
    • 環境変数でサイト名アプリ名を定義し、サイト名のディレクトリが存在しない場合、またはCLEARSITEでアプリを削除している場合、テンプレートを用いてアプリケーション雛形を構築する。
    • Django-admin startupprojectでサイトを構築、settings.pyの必要な部分を順次編集。
    • python manage.py startappで最初のアプリケーションを作成(これを用いてシングルページアプリケーションの雛形とする)
    • アプリケーションの設定部分をsettings.pyに追記編集。bootstrapなどのミドルウェアの設定、LOGGING設定、環境変数読み込み設定。
    • ルート用urls.py、フロント側にデフォルト変数を送る機構context_processors.pyの雛形作成
    • アプリケーション定義用urls.py、views.pyの雛形作成、indexページとajax待ち受けの最低限構成、およびテンプレートHTMLのindex.html、base.html雛形作成。
    • python manage.py migrate :sqliteデータベースの初期化、管理者ユーザーの作成
    • スタティック領域の構成
      • bootstrapはnpmのグローバルエリアからローカルコピー(これは最新版に対応するため常に行う)
      • サイト用カスタムscssのテンプレート作成(再構築時)
      • sass常駐コンパイラの起動(上記scssファイルの更新と同時に自動コンパイル)
      • bootstrap関連その他js、およびtinymce設定
      • DOM構築用javascriptモジュールテンプレート(TSソース)設置、およびTypeScript初期化と設定ファイル初期編集(再構築時)
      • tsc常駐コンパイラ起動

テンプレートに凝りすぎてほぼほぼ自由度が下がってしまっている件。前回のDOM構文もTypescriptに移植して使える状態にして用意してます。細かい解説は折りを見て追記します。

不正アクセスログ集計ツールその後

以前の記事で不正アクセスの疑いのあるIPアドレスを集計してアクセスブロックするツールを作成し運用していることと、docker導入に当たってブロックの仕組みを再検討して運用している記事を上げていましたが、もう一つポータルサイトのWEBアプリへの不正アクセス(django管理画面やwordpressへの不正ログイン)を集計する仕組みも紹介しました。実はこの仕組みに関してもdocker化で動作しなくなっていました。

データベースに保管された不正ログイン情報(Login_History)は別コンテナのデータベースサーバーにあるので、localhostからコンテナ名に変更するだけで良いのだが、ブロック済みのIPアドレスリストを取得して新規IPアドレスのみを表示する仕組みについては、ブロックリストがホストで管理されているためコンテナ内からはアクセス出来ない。ブロックリストを保管している/etc/ufw/before.rulesをコンテナに強引にマウントしてしまう方法もあるが、あまりスマートではない。

ということでこれまた興味本位の勉強を兼ねてホスト側からdockerブリッジネットワークへのサービスとしてソケット通信でブロックリストを公開する仕組みを作成した。

  • blocklist-server
#!/usr/bin/env python3
#--*-- coding: utf-8 --*--
# 対象ネットワーク: docker bridge network([shared] 192.168.29.0/24)
# ブロックリストホスト: 192.168.29.1
# 起動(su): /usr/local/sbin/blocklistsrv.py | /usr/bin/logger -i -t blocklistsrv 2>&1 &
# 停止: ps aux | grep blocklistsrv  -> kill -9
# ポート確認: lsof -i :8765  -> kill -9

import os, sys, socket
import psutil
import argparse
import re
import numpy as np

import daemon
from daemon import pidfile

pfile = '/var/run/blocklist-server.pid'

ipset_ufw = '/etc/ufw/before.rules'
ipset_start_line = 'blacklist-ipset-S'
ipset_end_line = 'blacklist-ipset-E'

svport = 8765
svhost = '192.168.29.1'

def getArgs():
    parser = argparse.ArgumentParser('blocklist service cmd')
    parser.add_argument('cmd', nargs='?', help='(start|stop|restart)')
    parser.add_argument('-l', '--syslog', action='store_true', help='enable syslog output')
    return parser.parse_args()

args = getArgs()

ーーー省略ーーー
def blserver():
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket1.bind((svhost, svport))
    socket1.listen(5)

    logger.info('Start Blocklist server. port : %d', (svport))
    while True:
        try:
            clientsocket, address = socket1.accept()
            blist = []
            logger.info(f"Connection from {address} has been established!")
            with open(ipset_ufw, 'r', encoding='utf-8') as fi:
                for line in fi:
                    if re.search(ipset_start_line, line):
                        break
                for line in fi:
                    if res:= re.match('^-A ufw-before-input -s ([0-9./]+) .*', line):
                        #logger.debug(res[1])
                        blist.append(res[1])
                    if re.search(ipset_end_line, line):
                        break
        #except KeyboardInterrupt:
        #    #clientsocket.close()
        #    exit('exit with keyboard interrupt!')
        except FileNotFoundError:
            logger.error('file not found! : %s' % (ipset_ufw))
        except PermissionError:
            logger.error('can\'t read %s, please run as superuser!' % (ipset_ufw))
        finally:
            clientsocket.send('\n'.join(blist).encode('utf-8'))
            clientsocket.close()

        ## blist 送信 

def main():
    #args = getArgs()
    #logger = getLoggerConf(args.syslog)
    if args.cmd == 'start':
        pass
    elif args.cmd == 'stop' or args.cmd == 'restart':
        # デーモンストップ
        if os.path.exists(pfile):
            try:
                logger.info(pfile + " is exits")
                pid =  open(pfile, 'r').read().rstrip()
                proc = psutil.Process(int(pid))
                proc.terminate()
                os.remove(pfile)
                logger.info('pid : %s removed.' % pid)
            except PermissionError:
                logger.error('PID File, permission error, please run as super user.')
                exit(0)
            #except:
            #    logger.error('some error occured.')
        if args.cmd == 'stop':
            exit(0)
    else:
        logger.error('operation command is one of (start|stop|restart)')
        exit(1)

    # デーモン起動
    with daemon.DaemonContext(pidfile=pidfile.TimeoutPIDLockFile(pfile)) as context:
        logger.info(context.is_open)
        blserver()


    #exit(0)



if __name__ == '__main__':
    main()


start, stop, restart機能を持つデーモンスクリプトを作成し、ubuntuホストへサービス登録

  • /usr/lib/systemd/system/blocklist.service
[Unit]
Description=blocklistsrv: this is service of ufw blocking IP address list.
After=docker.service

[Service]
ExecStart=/usr/local/sbin/blocklist-service -l start
ExecStop=/usr/local/sbin/blocklist-service -l stop
ExecReload=/usr/local/sbin/blocklist-service -l restart
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target

ここでsudo systemctl start blocklistでサービスが起動される。ちなみに恒久的に起動するようにするにはsudo systemctl emable blocklistとする。

クライアントであるdocker内のdjangoのコードも下記のようにソケット対応に変更。

blocklist_socket_server = ('192.168.29.1', 8765) 

def get_blocklist():                                                                                                    
    """                                                                                                                 
    root権限でブロックリスト取得コマンドを走らせて標準入力からブロックアドレスのリストを返す                            
    """                                                                                                                 
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                                                         
    socket1.connect(blocklist_socket_server)                                                                            
    bipset = [ ipv4adrset(ips) for ips in socket1.recv(4096).decode('utf-8').split('\n') ]                              
    return bipset                                                                                                       

とりあえず動作するようになったので一安心。

名前付きなUNIXソケットでも良かったけど、またソケットファイルをコンテナにマウント〜となってしまうので、dockerネットワークのゲートウェイから見せるソケットサーバーとしました。こんな感じで良いのだろうか。まぁ特に重要なサービスでもないので結果オーライで運用中です。

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なので飾りっ気一切無し。この辺の見栄え関連は次回以降勉強していくことにする。

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

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

パート3と思ったけど、ちょっとdjango開発環境にて問題発生。

Ubuntuサーバー(さくらVPS)上のコンテンツを編集するのに、いろいろゴタゴタした顛末を下記に

  • sshログイン出来るユーザーでremote-SSHのVScodeでdjangoプロジェクトのコンテンツ編集を行なう。
  • このままではプロジェクトのファイル所有者やアクセス権がユーザーのものになってしまうので、Apacheユーザーで動作する本番環境を直接編集するわけには行かない。
  • そこで一旦GitHubプライベートリポジトリにpushして、Apacheユーザーで本番環境にpullすることで動作させることとした。
  • しかし、ちょっとした修正さえ編集して動作を確認するためにpush/pullを繰り返すこととなり、Git履歴がメッチャ汚くなる(一旦pushしてしまうと例え一人で使用しているとはいえrebaseとか履歴編集が容易ではない)。
  • いっそApacheユーザーでSSHログイン出来るようにしようかと、血迷ったことを考えたが、すんでの所で思いとどまる。
  • 既にApache2環境にデプロイ済のポータルプロジェクトだが、ユーザー環境ではもう一度python manage.py runserverで動作させてある程度動作検証した後に本番環境へpushするという流れにしようとした。
  • 注意点は、データベースは同じものを使用するので壊さないように注意。
  • ログファイルなど本番用と混ざらないように、別ログになるようにする
# settings.py で動作中のユーザー名を得る(テスト環境であればローカルユーザーになる)
RUN_USER = pwd.getpwuid(os.stat(__file__).st_uid).pw_name

# ユーザー名でApache2で動作中かどうかを判別してログのベースディレクトリを変更
LogBase = '/var/log/django' if RUN_USER == APACHE_USER else os.path.join(BASE_DIR, '.log')

# あとはログの仕様で設定する
  • 一応デプロイ後のものと同じ構成でdjango runserverテスト環境でも動作することを確認できたが、画像であるとかstaticが読み込めなくなっていた。(Apacheではサーバーの設定でstaticのエイリアス設定をしていたので、djangoのURLconfでの動作はあまり見ていなかった)
  • 調べたところ、静的ファイルの設定はsettings.pyでSTATIC_URLとSTATIC_ROOTで設定してあったのだが、ウチの環境では動作せず(素の状態でdjango-admin startproject XXXX して試してみたがstaticディレクトリを読み取ることが出来なかった)。代わりにSTATICFILES_DIRSという配列を設定することで無事読み込めることが分かった。
STATIC_URL = '/static/'    # これはOK

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
#   ↓
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

  • STATIC_ROOTとSTATICFILES_DIRSを同時に設定するのはダメ。
  • djangoの素の状態でこれでstaticが読み込めることが確認できたが、本番環境からpullしてきたポータルプロジェクトでは上記の修正を行なってもstaticが読み込めなかった。数時間掛けて原因究明した結果、単に404 Not Foundの状態をブラウザが覚え続けていただけで、ブラウザキャッシュクリアすることで無事読み込めるようになった。お粗末。

何ともアホらいいオチでしたが、ブラウザキャッシュは意外に見落としがち。以前のサーバー引越の時も新サーバーに以前のドメインを割り当ててても一向に変化が無くて困ったのも、結局はブラウザキャッシュが原因でした。

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

目的

不正アクセスのログを集計してポータルの管理ページに結果を表示する

不正アクセスの定義

今回の場合はWEBページの認証機構へのログインアタックとする。パスワードを知っていて入って来てしまう場合は対象外。この場合はログイン履歴表示機能で様子をみることが可能とする。

機能

  • djangoポータルページの管理グループのみ表示(ページへのリンクも管理グループのみに表示)
  • django認証ログからの集計と、Wordpressのプラグイン(User Login History)の認証履歴データベーステーブルからの履歴も取得し、合計として集計する。
  • 集計開始日時ー終了日時、リストアップするための認証失敗回数(5回など)設定。
  • 特定のIPアドレスのアクセス詳細を表示ー上記リスト表示にボタン追加

その他機能追加として、直接関係は無いが

  • エラー404などのページカスタマイズ
  • djangoいろいろ勉強用テストページ

現在の所、最終的な完成形は上記のような仕様になるが、管理ページの追加なので訪問者には何も関係が無いです。その他思いついたら仕様追記する予定。

開発に関しては、djangoのプロジェクトディレクトリをgit管理とし、GitHubプライベートリポジトリ経由でsshログインユーザーにclone。VScodeで編集してテストブランチとしてpush。apacheユーザーでプロジェクトディレクトリ(テスト用仮想サーバー)にpullして動作試験。デバッグを行い完成形(公開レベルになった段階)をmasterブランチにmergeしてGitHubにpush。公開用プロジェクトディレクトリにpullして運用するという流れ。

単純にapacheユーザーのプロジェクトをVScodeのssh-remoteで編集したいという欲求のため、このような複雑は構造となってしまった。GitHubの使い方の勉強できて丁度良いか。

djangoセキュリティログ追加

チュートリアルを使用したdjangoの勉強と並行して、djangoで構成している本サイトのポータルページもある程度のセキュリティー対策が急がれる。djangoを素で使用しているサイトは少ないようでwordpressのようにBOTによる攻撃はまだ見られない。何らかのCMSを使用しているのを見越して適当なURLをぶっ込んでくるBOTは404で弾けるのであまり問題にならないが、ごくたまーに、しつこくadminでログインしようとする輩が現れるようだ。

このWordpressのサイトでも結構多かったので(普通に見に来てくれる人は殆ど居ないのにwww)、User Login Historyというプラグインを入れている。ここもたまに100回以上のadminログイン試行をしてくる輩がいるので、このログを利用してIPを特定しblockset.pyで手動だけどお引き取り願うことにしている。

同じような仕組みをdjangoのポータルページにも仕掛けようと勉強がてら色々と調べたところ、ログイン用の認証バックエンドという仕組みが利用できそうだということが分かった。デフォルトの<project>/setting.pyには明示的に定義はされていないが(django.conf.global_settings にデフォルト定義)、AUTHENTICATION_BACKENDSという配列がありここのカスタムや追加の認証バックエンドを追加出来る(<project>/settings にて再定義)

デフォルトでは’django.contorib.auth.backends.ModelBackend’が一つ定義されているので、こちらを継承カスタマイズして認証結果をログ出力できるようにする方針とする。

django.contrib.auth.backends.ModelBackend 抜粋
class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user
       ・・・・・・

認証判定にはメソッドauthenticateが機能しており、認証OKであればusernameを、NGであればNoneを返す関数であるため、クラス継承により下記のようにメソッドをオーバーライドして再定義する

<project>/backends.py
from django.contrib.auth.backends import ModelBackend
from logging import getLogger # using logging module

class ModelLogBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        logger = getLogger('loginInfo') # example
        res = super().authenticate(request, username, password, **kwargs)
        if res is None:
            logger.info('login faild, bad user: %s' % username)
        else:
            logger.info('user logged in : %s' % username)
        return res

super()で親クラスのメゾッドに丸投げして、結果に応じてログを吐き出す機構を追加。継承したクラスを新たなバックエンドとして<project>/settings.pyに定義する。

# Authntication backend
AUTHENTICATION_BACKENDS = [
    '<project>.backends.ModelLogBackend'
]

<project>はdjangoのプロジェクト名に置き換える。

ちなみにloggerの設定は別の記事を参照して完成しておくこと。当サイトの場合は他のアクセスログの都合でstructlogを使用している(設定さえしておけばloggingと同等に使用可能)。

もともと、ポータルとしてのdjangoサイトは出来上がっていたので、この機能を付けたくてdjangoの本格勉強を始めたという流れでした。蓋を開けてみると思ったより簡単で、同時にカスタマイズ性の高さに今更ながら感動です。次はdjabngo+bootstrap(japascript)による動的サイトの実装実験を目指したいな。

djangoチュートリアルメモ

基本的にはdjangoサイトのチュートリアルをそのままトレース。やった事だけをメモしていこうかと。。。

  • 仮想環境への移行 source bin/activate
  • djangoインストール : pip install Django
  • バージョン確認:python -m django –version → 3.2.4
  • プロジェクト作成:django-admin startproject mysite
  • とりあえずテストサーバー起動:python manage.py runserver 0:8000
    外部からアクセスするため(ただしファイヤーウォールで接続制限をかけること)
  • 「Invalid HTTP_HOST header」エラーが出るので、setting.pyを修正
    ALLOWED_HOSTS = [‘*’]
    デフォルトでは空になっているので、自サイト以外からはアクセスできないようだ
  • チュートリアルに沿ってアプリケーション作成
    python manage.py startapp polls
  • ここまで作った状態で、プロジェクト下とライブラリのdjango下をそれぞれgitの管理化に入れて、pythonの勉強を兼ねて、動作の仕組みを解析する予定。

サーバー設定作業メモ

何をやったか忘れないように、メモメモ記事を残すことにした。これも投稿数稼ぎかなw

ポータルサイト http://www.marochanet.org/ の djangoアップデート作業

ウェブユーザでpython仮想環境に入り、まずはpipのアップデート

(venv0) xxx@jagha:~/html/python$ python -m pip install --upgrade pip

次に仮想環境のアップデート可能なモジュールリストをとる(一部表示)

(venv0) xxx@jagha:~/html/python$ pip list -
Package                Version Latest Type
---------------------- ------- ------ -----
……
Django                 3.0.8   3.2.4  wheel
django-bootstrap4      2.2.0   3.0.1  wheel
django-hitcount        1.3.2   1.3.3  wheel
django-tinymce         3.0.2   3.3.0  wheel
……

他にもリストアップされたモジュールを全てアップデートする

(venv0) xxx@jagha:~/html/python$ pip install -U (module name)

ここで、リストには無いがデータベースエンジンとの接続に必要なmysqlclientのアップデートに失敗。

これに関してはpython側のライブラリが不足していたため下記コマンドをrootで打って、ライブラリをインストールする

apt-get install python3-dev libmysqlclient-dev

無事mysqlclientのアップデートに成功して終了。