主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

スポンサーサイト

                
tags:
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Python-Tornado: GoogleAppEngineで動かしているブログをTornadoで書き換える

                
 後学のためにGoogleAppEngineで動かしているブログを、Tornadoの自サーバで動くように書き換えてみる。どういう書き換えが必要になるだろうか。完全に等価になるようにでなく、少しざっくり書くので要検証。データベースもGoogleAppEngineのものは使えないので、別のものを用意する。今回は使いやすいしパフォーマンスもいいMongoDBを使うことにする。MongoDBのドライバとしてMotorを使う。

 参考としてTornadoのブログデモを読んだ。

 TornadoはGoogleAppEngineで使用されているwebapp2と似た書き方をする。だからそこそこ似たコードになる。

1.リクエストハンドラの定義
GAE
class MainHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
self.response.write('foo')

def post(self):
self.response.headers['Content-Type'] = 'text/plain'
self.response.write('foo')


Tornado
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.set_header("Content-Type", "text/plain")
self.write("foo")

def post(self):
self.set_header("Content-Type", "text/plain")
self.write("foo")



2.テンプレートエンジンの変更
 GAEではjinja2を使っていたが、Tornadoでは自前でパフォーマンスの優れたテンプレートエンジンを用意しているというので乗り換えてみる。
GAE
import jinja2
jinja_environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
autoescape=True)

class MainHandler(webapp2.RequestHandler):
def get(self):
    template_values = {}
##
template = jinja_environment.get_template(file_name)
self.response.out.write(template.render(template_values))

Tornado
class MainHandler(tornado.web.RequestHandler):
def get(self):
    template_values = {}
##
self.render(file_name, **template_values)

settings = {
template_path=os.path.join(os.path.dirname(__file__), "templates"),
}
application = tornado.web.Application([
(r"/", MainHandler),
], **settings)

あとはテンプレートの文法として、任意のテンプレートの値を書きたければTornadoもGAEと同じで{{ foo }}。jinja2では{% for doc in docs %}{{ doc|safe }}{% endfor %}だったものが、 {% for doc in docs %}{% raw doc %}{% end %}になってforやifのあとの終端マークはendのみで表す。


3.認証済セッション
 GAEではリクエストハンドラのgetメソッドやpostメソッドにlogin_requiredというデコレータを付ければGoogleの認証を使わせてもらえた。Tornadoはそんなものはもちろん用意されていないので、自分で認証を与えるものを書く必要がある。OpenIDが使えるのでそれを使うのもあり。
 TornadoのブログデモやWeb上にあるTornadoでの認証を扱う記事では、web.ReqestHandlerを継承して、get_curernt_userメソッドをオーバーライドすることでauthenticatedデコレータを使用可能にして認証済みかそうでないかを分けていた。で、get_current_userメソッドではクッキーに入れられたユーザIDを呼び出す処理が書かれていて、それがあまりよろしくないと思ったのでlogin_requiredメソッドを用意した。クッキーの盗聴や改ざんを考えると、クッキーに認証情報として永続的に使う個人IDを入れるのはまずいんでないだろうか。使い捨てのセッションIDにするべきではないんだろか。それを考えてlogin_requiredメソッドは、クライアントから飛ばされたクッキーに認証済みのセッションIDが入っているかを、MongoDBに確認するメソッドにした。ちょっと見慣れない書き方だけどそれは次の非同期処理で。
Tornado
class BaseHandler(tornado.web.RequestHandler):
def login_required(self):
session = self.get_secure_cookie("session")
doc = yield db.session.find({"session":session})
if doc:
return session
else:
raise tornado.web.HTTPError(401)

class ManageHandler(BaseHandler):
@tornado.gen.coroutine
def get(self, page=0):
session = self.login_required()


4.非同期処理
 Tornadoの特徴である非同期処理。HTTPリクエストなどにこの非同期処理が適用されるので、MongoDBへのアクセスもやっぱり非同期処理で行われる。以前はコールバック関数を使って一連の処理が終了するまでを書いていたが、最近はTornadoもMonngoDBドライバのMotorも開発が進んで、自分でコールバック関数を書かなくても一連の処理を行えるようになった。
 書き方としては非同期処理が行われる関数やメソッドにはgen.coroutineデコレータを付ける。そしてそのメソッドや関数を呼ぶときはyieldを付けて呼び、返り値はreturnでなくtornado.gen.Returnを使う。
Tornado
class FooHandler(BaseHandler):
@tornado.gen.coroutine
def get(self):
docs = yield self.get_doc()
self.render(file_name, entries = docs)

@tornado.gen.coroutine
def get_doc(self)
docs = yield db.entries.find().to_list(length=5) # asynchronous query to MongoDB
tornado.gen.Return(docs)



5.SSL通信
 クッキーで認証を扱うので通信をSSLにすることは欠かせない。GAEではapp.yamlで一行付加するだけだったが、
GAEでは自前で.crtファイルと.keyファイルを用意しなければならない。用意できたらサーバ設定でファイルの場所を教えてやるだけでOK。
Tornado
ssl_settings = dict(ssl_options = {"certfile": os.path.join("crt/tor.crt"),
"keyfile": os.path.join("crt/tor.key"),
},
)

http_server = tornado.httpserver.HTTPServer(
tornado.web.Application([('/(\d*)', TopHandler),
('/article/([\w\-]+)', ArticleHandler),
],
**settings),
**ssl_settings
)



 ここまでに書いたことをまとめて、ブログのコード部分は下記のとおりに。XSRF対策などまだセキュリティ面での検討事項を残している。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datetime
import os
import re
from urllib2 import unquote
from urllib2 import quote
import json
import datetime
from math import ceil

import tornado.auth
import tornado.ioloop
import tornado.web
import tornado.gen
import tornado.httpserver
import pymongo
import motor
from bson.objectid import ObjectId


ARTICLES_IN_PAGE = 15
EMAIL_LIST = set(["*****@*******"])

############################################# general

class ArticleHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self, entry_id):
""""""
entry = yield db.articles.find_one({"_id":entry_id})
if not entry:
self.redirect("/")
self.render("article.html", entry=entry)

class TopHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self, page=0):
""""""
page = int(page) if page else 0
article_count = yield db.articles.find().count()
articles = yield db.articles.find({},
skip=ARTICLES_IN_PAGE*page,
sort=[("date", -1)]
).to_list(length=ARTICLES_IN_PAGE)
article_count = yield db.articles.find().count()
pages = ceil(float(article_count) / ARTICLES_IN_PAGE)
template_values = {"current_page":page,
"pages":range(int(pages)),
"entries":articles,
"admin":False,
}
self.render("toppage.html", **template_values)


############################################# admin

class GoogleLoginHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
@tornado.gen.coroutine
def get(self):
self.clear_cookie("session")
if self.get_argument("openid.mode", None):
user = yield self.get_authenticated_user()
if user["email"] in EMAIL_LIST:
id_ = yield db.session.insert({"user":user["claimed_id"],
"createdAt":datetime.datetime.utcnow()})
self.set_secure_cookie("session", str(id_), secure=True)
self.redirect(r"/admin/manage/0")
else:
self.write("Not Registered ID")
else:
yield self.authenticate_redirect()

class LogoutHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
yield db.session.remove({"_id":ObjectId(self.get_secure_cookie("session"))})
self.clear_cookie("session")
self.write("bye bye")

class BaseHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def login_required(self):
doc = yield db.session.find_one({"_id":ObjectId(self.get_secure_cookie("session"))})
if doc:
raise tornado.gen.Return(doc["user"])
else:
self.clear_cookie("session")
raise tornado.web.HTTPError(401)

class ManageHandler(BaseHandler):
@tornado.gen.coroutine
def get(self, page=0):
""""""
page = int(page)
g_id = yield self.login_required()
entries = yield db.articles.find(skip=ARTICLES_IN_PAGE*page,
sort=[("date", -1)],
).to_list(length=ARTICLES_IN_PAGE)

article_count = yield db.articles.find().count()
pages = ceil(float(article_count) / ARTICLES_IN_PAGE)
template_values = {"current_page":page,
"pages":range(int(pages)),
"entries":entries,
"admin":True,
}
self.render('toppage.html', **template_values)


class AddHandler(BaseHandler):
@tornado.gen.coroutine
def get(self):
""""""
yield self.login_required()
self.render("add.html", entry={})

@tornado.gen.coroutine
def post(self):
""""""
user = yield self.login_required()
entry_id = self.get_argument("url", "")
if not entry_id:
entry_id = str(datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
elif (not re.match("[\w-]{{{0}}}".format(len(entry_id)), entry_id)) or\
(yield db.articles.find_one({"_id":entry_id, "user":user})):
entry = {"_id":entry_id,
"title":self.get_argument("title"),
"head":self.get_argument("head"),
"link":self.get_argument("link"),
"body":self.get_argument("body"),
"image":self.get_argument("image"),
}
self.render('add.html', entry=entry)
raise tornado.gen.Return()
doc = {"_id":entry_id,
"title":self.get_argument("title"),
"head":self.get_argument("head"),
"link":self.get_argument("link"),
"body":self.get_argument("body").replace("\n", "<br>"),
"date":datetime.datetime.now(),
"user":user,
}
if self.get_argument('image').startswith('http://'):
doc.update({"image":self.request.get('image')})

yield db.articles.save(doc)
self.redirect('/admin/manage/0')

class EditHandler(BaseHandler):
@tornado.gen.coroutine
def get(self, entry_id):
""""""
user = yield self.login_required()
entry = yield db.articles.find_one({"_id":entry_id, "user":user})
if not entry:
self.redirect('/admin/manage/0')
self.render('edit.html', entry=entry)

@tornado.gen.coroutine
def post(self, entry_id):
""""""
user = yield self.login_required()
old = yield db.articles.find_one({"_id":entry_id, "user":user})
if not old:
raise tornado.web.HTTPError(501)
entry = {"_id":entry_id,
"title":self.get_argument('title'),
"link":self.get_argument('link'),
"body":self.get_argument('body').replace("\n", "<br>"),
"head":self.get_argument('head'),
"date":old["date"],
"user":user,
}
if self.get_argument('image').startswith('http://'):
entry.update({"image":self.get_argument('image')})
yield db.articles.save(entry)
self.redirect('/admin/manage/0')

class DeleteHandler(BaseHandler):
@tornado.gen.coroutine
def post(self, entry_id):
""""""
user = yield self.login_required()
yield db.articles.remove({"_id":entry_id, "user":user})
self.redirect('/admin/manage/0')


############################################# initialize and run
def init_db(HOST, PORT):
c = pymongo.MongoClient(HOST, PORT)
db = c.test_db
db.session.ensure_index("createdAt", expireAfterSeconds = 6 * 60 * 60)
db.articles.ensure_index([("date", -1)])
c.close()

db = motor.MotorClient(HOST, PORT).test_db
return db

if __name__ == "__main__":
DB_HOST = "localhost"
DB_PORT = 27017
db = init_db(DB_HOST, DB_PORT)
settings = dict(cookie_secret="*******************",
static_path=os.path.join(os.path.dirname(__file__), "static"),
template_path=os.path.join(os.path.dirname(__file__), "templates"),
login_url="/admin/login",
xsrf_cookies=False,
autoescape="xhtml_escape",
debug=True,
)
## application = tornado.web.Application([('/(\d*)', View),
## ('/article/([\w\-]+)', Article)],
## **settings)
## application.listen(8888)

ssl_settings = dict(ssl_options = {"certfile": os.path.join("crt/tor.crt"),
"keyfile": os.path.join("crt/tor.key"),
},
)
http_server = tornado.httpserver.HTTPServer(
tornado.web.Application([('/(\d*)', TopHandler),
('/article/([\w\-]+)', ArticleHandler),
('/admin/login', GoogleLoginHandler),
('/admin/logout', LogoutHandler),
('/admin/manage/(\d+)', ManageHandler),
('/admin/add', AddHandler),
('/admin/edit/([\w\-]+)', EditHandler),
('/admin/del/([\w\-]+)', DeleteHandler),
],
**settings),
**ssl_settings
)
http_server.listen(8890)
tornado.ioloop.IOLoop.instance().start()
            

コメントの投稿

非公開コメント

プロフィール

hMatoba

Author:hMatoba
Github

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。