Google App Engineサイトを活用して家のWANアドレス管理サーバーを構築する
開発環境は前回整えたので、サーバーアプリ作ってみた。と言ってもやることは
ごく簡単なことなのでAppEngineの知識はチュートリアルレベルで十分。
アプリケーションを定義するapp.yamlは以下の様な感じ。
[app.yaml]
application: アプリ名
version: 1
runtime: python27
api_version: 1
threadsafe: yes
handlers:
- url: /favicon\.ico
static_files: favicon.ico
upload: favicon\.ico
- url: .*
script: main.app
# login: required
libraries:
- name: webapp2
version: "2.5.1"
サンプルをコピペしてapplication: のところを変更しただけかな。
アプリにアクセスした時に起動するメインプログラムがこちら。
[main.py]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import webapp2
from google.appengine.api import users
from google.appengine.ext import db
from datetime import datetime,timedelta
class AppUser(db.Model):
account = db.UserProperty()
lanaddr = db.StringProperty()
address = db.StringProperty()
apppath = db.StringProperty()
updated = db.DateTimeProperty()
class RootHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
self.response.out.write( self.request.remote_addr )
class MyHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/html'
user = users.get_current_user()
if user:
self.response.out.write(''+user.email()+'')
app_users = AppUser.gql("where account = :1",user)
for au in app_users:
if self.request.remote_addr == au.address:
url = "http://"+au.lanaddr+au.apppath
non = "http://"+au.address+au.apppath
else:
url = "http://"+au.address+au.apppath
non = "http://"+au.lanaddr+au.apppath
self.response.out.write(""+url+"
")
self.response.out.write(non+"
")
else:
self.redirect(users.create_login_url(self.request.uri))
class CommitHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
user = users.get_current_user()
if user:
app_users = AppUser.gql("where account = :1",user)
if app_users.count() == 0:
ip = AppUser()
else:
ip = app_users[0]
ip.account = user
ip.updated = datetime.now()
ip.address = self.request.remote_addr
ip.apppath = self.request.get('path')
ip.lanaddr = self.request.get('lan')
if ip.lanaddr == "":
ip.lanaddr = "localhost"
ip.put()
self.response.out.write(ip.address+" "+ip.lanaddr+" "+ip.apppath+" COMMITED")
else:
self.redirect(users.create_login_url(self.request.uri))
class AdminHandler(webapp2.RequestHandler):
def get(self):
self.response.headers['Content-Type'] = 'text/plain'
user = users.get_current_user()
if user:
if users.is_current_user_admin():
self.response.out.write('AppUsers')
for u in AppUser().all():
self.response.out.write("\n"+u.account.email())
self.response.out.write(" "+u.lanaddr)
self.response.out.write(" "+u.address)
self.response.out.write(" "+u.apppath)
self.response.out.write(" "+str(u.updated+timedelta(hours=9)))
else:
self.redirect(users.create_login_url(self.request.uri))
app = webapp2.WSGIApplication([('/', RootHandler),
('/my', MyHandler),
('/commit', CommitHandler),
('/admin', AdminHandler)
],
debug=True)
サンプルをコピーして、必要なコードを書き加えていったところ、こんななりました。
Apacheライセンスである事が記述されてますが、サンプルがそうなっていたのでそのまま残してます。自分はそんなつもりはないんだけど。
順を追って説明しておくと
import webapp2
'webapp2'フレームワークを使いますってこと。サンプルがそうなってた。
from google.appengine.api import users
ユーザサービスというアカウント認証関係のAPIを使用する。WANアドレスをこのアカウントと紐付けるため。
from google.appengine.ext import db
データストアというデータベースを使用する。これ使ってデータ管理する。
from datetime import datetime,timedelta
これがちょっと悩んだんですが、サーバー側の時間が全部UTCなんですね。そこからJSTへ変換するための
最小モジュールをインポート。pythonモジュールで時間関係の便利なモジュールは沢山あるけれど、やりたいことはたった一つ
self.response.out.write(" "+str(u.updated+timedelta(hours=9)))
の部分なので、timedelta使うことに。
この辺は、'google app engine utc jst'でググれば沢山親切ページが現れる。
以下の様な定義を入れておくことで、データストアにAppUserという自分専用のデータテーブルが作成される
class AppUser(db.Model):
account = ユーザアカウント(Googleアカウントオブジェクトインスタンス)
lanaddr = LANアドレス(コミット時に自分でパラメータ指定する)
address = WANアドレス(self.request.remote_addrで得られる。目的の物!)
apppath = 自分ちのサーバーアプリへのURL(コミット時に自分でパラメータ指定する)
updated = コミット時の更新日時
プログラム上では、この'AppUser'クラスを使ってDBへアクセスする。
最初に実行されるフレームワークアプリケーションオブジェクト生成部分でハンドラの定義をする。
app = webapp2.WSGIApplication([('/', RootHandler),
('/my', MyHandler),
('/commit', CommitHandler),
('/admin', AdminHandler)
http://[app-name].appspot.com だったら、RootHandler
http://[app-name].appspot.com/my だったら、MyHandler
http://[app-name].appspot.com/commit だったら、CommitHandler
http://[app-name].appspot.com/admin だったら、AdminHandler
で、それぞれの'get'メソッドが呼ばれる。Railsでいうところのroutesかな。
class RootHandler(webapp2.RequestHandler):
ここはとにかく単純にWANアドレスを返す。そういうサイトよくあるよね。
バッチ処理とかで使いやすいようにプレーンテキストでアドレスのみを返すようにした。結構便利。
class MyHandler(webapp2.RequestHandler):
自分のアドレスを返すところ。
ログインユーザのアカウントに紐付いたデータが登録されていれば、それをリンクで表示。これがやりたかっただけなんだけど、道のり長いっす。
ログインしていないなら、ログインページへ飛ばす。ログインしたら戻ってくる。
self.redirect(users.create_login_url(self.request.uri))
がフレームワークとユーザサービスの便利な組み合わせ。
これもチュートリアルの範囲内。
class CommitHandler(webapp2.RequestHandler):
"http://[app-name].appspot.com/commit?lan=XXXX&path=YYYY" ってアクセスする。
アカウント認証されたユーザならば、DB検索してマッチしたデータエンティティに、なければ新たなエンティティに
データを記録するところ。ログインしてなければ同じようにログインページへ飛ぶ。
class AdminHandler(webapp2.RequestHandler):
ここはデータテーブルの一覧を表示するところ。管理者(自分)のみが観覧できる。
チュートリアルに従って作って、ブラウザ上からアクセスして機能テスト。問題なし!
実際にはここまで作るだけでも試行錯誤したけど・・・本題とは遠いところで詰まった。
例えば、UTC=>JSTのシンプル解決に悩んだりとか。
適当なモジュールをインポートしてもサーバーになかったりとか。
途中からDBモデルにlanaddrを追加したんだけれど、なぜか更新されないとか。
'https://appengine.google.com'のアプリ管理からデータテーブルを削除したり、ログ見たり・・・
だけど、一番苦労したのは、ログイン認証だ。
サーバーは出来たが誰でもアクセスできちゃう。管理ユーザ(自分)だけしかアクセスできないパーミッション設定が可能だけど、それはそれで面倒だ。
個人のデータを保持するにはやはり個人を特定する認証が必要なのだ。
(もし利用希望者がおられれば、URLお教えしますが・・・)
ブラウザからならログインページが出て、ログインしたら後はブラウザがよろしくやってくれるけど、
やりたいのは自分ちのUbuntuServerから、cronタスクで処理したいので、ブラウザがやってることを自分でやらんとならん。
googleアカウント認証は、こちらを参考にしましたよ
http://johannilsson.com/2011/04/13/authenticated-requests-on-app-engine.html
http://maimon-it.blogspot.jp/2010/04/authenticating-google-app-engine-apps.html
スクリプトでのログイン認証と、認証付きアクセスを記述したものがこちら
[gauth.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-
from subprocess import *
from os import path
mail = 'Googleメールアドレス'
password = 'パスワード'
app = 'アプリ名'
app_url = 'http://'+app+'.appspot.com/'
cookie_file = path.expanduser("~")+'/.google_myapp_cookie'
def login():
try:
# API認証キーを取得
print 'Get Auth-Key...\n'
auth = Popen(['curl','-f','-s',
'https://www.google.com/accounts/ClientLogin',
'-d','accountType=HOSTED_OR_GOOGLE',
'-d','Email=' + mail,
'-d','Passwd=' + password,
'-d','source=' + app,
'-d','service=ah'],
stdout=PIPE).communicate()[0]
# ログインしてクッキー取得
print 'Login...\n'
login = Popen(['curl','-c','-',
app_url+'_ah/login?auth='+auth[ auth.rindex("Auth=")+5:-1]],
stdout=PIPE).communicate()[0]
cookie = login[login.rindex("ACSID")+6:-1]
# クッキーの保存
f = open(cookie_file,'w')
f.write(cookie)
f.close()
return cookie
except:
return ""
def get(controller):
try:
f = open(cookie_file)
cookie = f.read()
f.close()
except:
cookie = login()
try:
return Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
stdout=PIPE).communicate()[0]
except:
return ''
pythonのsubprocessや、外部コマンド'curl'については、親切解説しているところが沢山あるんで。そちらで、よろしくです。
処理内容を簡単に説明する
loginメソッド:(ログインしてクッキーを得るまで)
'https://www.google.com/accounts/ClientLogin' へ所定のパラメータ付きでアクセスしてアカウント認証データを取得。
'http://[app-name].appspot.com/_ah/login?auth=認証データのAuth=以降のデータ列' ってやるとログイン出来る。
ログインした時に得られるデータ内に"ACSID"というキーワードのデータ列があるんで、その部分を切り出して保存。
これがこのサイトへのログインクッキーデータ。
ドキュメントによると、Googleアカウントだけじゃなく、OpenIDでの認証も出来るらしいけど、その辺は無視。
getメソッド:(クッキー使ってアクセス)
クッキーデータをファイルから読んで、認証付きで指定URLへGETアクセスする。
手順がやっつけっぽいが、一応機能する。
Cookieが無効になる期限がどれくらいなのかイマイチ不明なので、
その辺調べてはっきりしてから、ちゃんとした手順に直そうかな。
・・・
ということで、修正した getメソッドがこちら
def get(controller):
for i in range(2):
try:
f = open(cookie_file)
cookie = f.read()
f.close()
ret = Popen(['curl',app_url+controller,'-b','ACSID='+cookie],
stdout=PIPE).communicate()[0]
if ret.find('COMMITED')>0:
return 'ok'
else:
raise
except:
cookie = login()
return 'err'
Cookieが無効になるのは、https://appengine.google.com から、アプリ設定ページで
Cookie Expirationというところで設定するのだった。1日、1週間、2週間が設定できる。デフォルトは1日だった。最長の2週間に設定したがそれでも短いので、コミットに失敗したらログインからやり直しするように変更した。
cronで実行するスクリプト
[gcommith.py]
#!/usr/bin/python
# -*- coding: utf-8 -*-
import gauth
# コミット
#gauth.get('commit?lan=192.168.X.Y&path=/foo')
gauth.get('commit?lan=192.168.X.Y')
cronタスク
crontabでユーザレベルタスクで回してます。
$ crontab -e
ってやって、自分の好きなエディタで開いたら
0 3 * * * python ~/Grive/gcommit.py
って書いておく。毎日3時にコミット実行です。crontabで回すと自分にメールでタスク結果が飛んでくる。
開発中は飛んでくるのが便利だけど、落ち着くとうざいね。止めちゃおっかな。
この記事をよく読んだ奇特な方はおや?と思ったかも。
gcommit.pyの冒頭に、#!/usr/bin/pythonって書いてあるのに、なぜに「python ~/Grive/gcommit.py」なのかと。
その謎は、パス名[/Grive]に隠されております。
どうでも良い話ですが・・・次回はその話にちょっと脱線します。