公開中の認証プロキシエンドポイントAPI
RESTを使用しているクライアントアプリケーションの場合、従来のAmazonアソシエイトWebサービスAPI(REST)で使用していた、
http://webservices.amazon.co.jp/onca/xml http://ecs.amazonaws.jp/onca/xml http://xml-jp.amznxslt.com/onca/xml
といったエンドポイントを、
http://honnomemo.appspot.com/paproxy
http://honnomemo.appspot.com/rpaproxy/jp/
に置き換えることで(クエリはそのまま)Product Advertising APIの認証処理を意識せずとも従来と同等に動作する……はず。
ご自分のGoogle App Engine上で動作させたい場合、以下のソースを参照のこと。
2009/07/09現在の最新の環境は→こちら(ZIP圧縮)。
のソースも入ってます。
そろそろGitとか使いはじめるべきかなぁ……。
ソースコード
app.yaml(以下の設定を追加)
- url: /paproxy.* script: paproxy.py
paproxy.py(プロキシ本体)
# -*- coding: utf-8 -*- """ paproxy.py for Google App Engine ■概要 Product Advertising API(旧Amazon アソシエイト Web サービス)用認証プロキシ ■使用方法 http://(設置ドメイン)/paproxy[/(locale)/]?(Product Advertising APIに渡す各種パラメータ) """ import datetime,urllib,hmac,hashlib,base64 import yaml,logging import wsgiref.handlers import re,random from google.appengine.ext import webapp from google.appengine.api import urlfetch from google.appengine.ext import db from urlparse import urlparse # === リダイレクト設定 USE_REDIRECT=True # True:Signature付きURLへリダイレクト False:Signature付きURLの内容を取得して返す # === アクセス制限 ACCESS_PERIOD_SECONDS=60 # 集計期間(秒) #ACCESS_LIMIT=10*ACCESS_PERIOD_SECONDS # 集計期間中のアクセス数上限 (0:制限なし) ACCESS_LIMIT=0 # 集計期間中のアクセス数上限 (0:制限なし) # === 設定ファイル(paproxy.yaml)読込み BASEPOINT=u'/paproxy' try: paproxy_conf=yaml.load(open('paproxy.yaml').read().decode('utf-8')) except: paproxy_conf={} for key in paproxy_conf.keys(): paproxy_conf[key]=unicode(paproxy_conf[key]) AWSAccessKeyId=paproxy_conf.get('AWSAccessKeyId',u'99999999999999999999') SecretAccessKey=paproxy_conf.get('SecretAccessKey',u'1234567890123456789012345678901234567890') DefaultValues = { 'AssociateTag' : paproxy_conf.get('DefaultAssociateTag','furyutei-22'), 'Service' : 'AWSECommerceService', 'Version' : '2009-01-06', } IgnoreKeys = ['SubscriptionId','AWSAccessKeyId','Signature','Timestamp',] RequestEndPoints = { 'ca': 'ecs.amazonaws.ca', 'de': 'ecs.amazonaws.de', 'fr': 'ecs.amazonaws.fr', 'jp': 'ecs.amazonaws.jp', 'uk': 'ecs.amazonaws.co.uk', 'us': 'ecs.amazonaws.com', } XsltEndPoints = { 'ca': 'xml-ca.amznxslt.com', 'de': 'xml-de.amznxslt.com', 'fr': 'xml-fr.amznxslt.com', 'jp': 'xml-jp.amznxslt.com', 'uk': 'xml-uk.amznxslt.com', 'us': 'xml-us.amznxslt.com', } RequestMethod = 'GET' DefaultLocale = paproxy_conf.get('DefaultLocale','jp') RequestEndPoint = paproxy_conf.get('RequestEndPoint',RequestEndPoints.get(DefaultLocale,RequestEndPoints['jp'])) XsltEndPoint = paproxy_conf.get('XsltEndPoint',XsltEndPoints.get(DefaultLocale,XsltEndPoints['jp'])) RequestPath = '/onca/xml' DB_FETCH_LIMIT=1000 #{ // dbPapAccessInfo() class dbPapAccessInfo(db.Model): date=db.DateTimeProperty(auto_now_add=True) #} // end of dbPapAccessInfo() #{ // checkBusy() def checkBusy(): if ACCESS_LIMIT==0: return False threshold=datetime.datetime.utcnow()-datetime.timedelta(seconds=ACCESS_PERIOD_SECONDS) try: while True: qdel=db.GqlQuery('SELECT * FROM dbPapAccessInfo WHERE date < :1',threshold) if qdel.count()<1: break vdels=qdel.fetch(DB_FETCH_LIMIT) if 0<len(vdels): db.delete(vdels) except: pass qinfo=db.GqlQuery('SELECT * FROM dbPapAccessInfo WHERE date >= :1',threshold) if ACCESS_LIMIT<1+qinfo.count(): return True accessInfo=dbPapAccessInfo() for ci in range(3): try: db.put(accessInfo) break except: pass return False #} // end of checkBusy() #{ // paproxy() class paproxy(webapp.RequestHandler): def get(self): (req,rsp) = (self.request,self.response) if checkBusy(): rsp.set_status(503) status='503: Service Temporarily Unavailable' rsp.out.write('<html><head><title>%s - paproxy</title></head><body><h1>%s</h1><p>Please try again later.</p></body>' % (status,status)) return use_redirect=USE_REDIRECT if not use_redirect: user_agent=req.headers.get('User-Agent',u'') if re.search(u'rpaproxy',user_agent,re.I): use_redirect=True # User-Agentがrpaproxyだったら強制的にリダイレクト使用 logging.debug(u'Use Redirect (User-Agent:%s)' % (user_agent)) args = {} for key in req.arguments(): if key in IgnoreKeys: continue args[key] = req.get(key) for key in DefaultValues.keys(): if not args.get(key): args[key] = DefaultValues[key] args['AWSAccessKeyId'] = AWSAccessKeyId args['Timestamp'] = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") # datetime.datetime.utcnow().isoformat() params = [] for key in sorted(args.keys()): params.append('%s=%s' % (urllib.quote(key.encode('utf-8'),safe='~'),urllib.quote(args[key].encode('utf-8'),safe='~'))) # '~'はquoteしない、'/'はする # ・Product Advertising APIの仕様上、RFC3986の非予約文字("A-Z", "a-z", "0-9", "-", "_", ".", "~")はエンコードしてはいけない # ・urllib.quoteのデフォルトでは、"A-Z", "a-z", "0-9", "-", "_", ".", "/"をエンコードしない (デフォルトオプション:safe="/") # →[差分]エンコードする:"/"、エンコードしない:"~" param_str = '&'.join(params) locale='default' mrslt=re.search(BASEPOINT+u'/([a-z]{2})/',req.uri) if mrslt: locale=mrslt.group(1).lower() if args.get('Style'): _endpoint = XsltEndPoints.get(locale,XsltEndPoint) else: _endpoint = RequestEndPoints.get(locale,RequestEndPoint) prefixes ='\n'.join([RequestMethod,_endpoint,RequestPath]) # (HTTPVerb,ValueOfHostHeaderInLowercase,HTTPRequestURI) apiurl = 'http://%s%s' % (_endpoint,RequestPath) signature = base64.b64encode(hmac.new(SecretAccessKey, '\n'.join([prefixes,param_str]), hashlib.sha256).digest()) url = u'%s?%s&Signature=%s' % (apiurl,param_str,urllib.quote(signature,safe='~')) logging.debug(url) if use_redirect: self.redirect(url) else: result = urlfetch.fetch(url=url,method=urlfetch.GET,allow_truncated=True,follow_redirects=True,deadline=10) statcode = result.status_code rsp.set_status(statcode) ctype = result.headers.get('Content-Type') if ctype: rsp.headers['Content-Type'] = ctype else: #rsp.headers['Content-Type'] = 'text/xml; charset=utf-8' rsp.headers['Content-Type'] = 'text/plain' try: rsp.out.write(result.content) except: pass #} // end of paproxy() def main(): application = webapp.WSGIApplication([ (BASEPOINT+u'.*', paproxy), ],debug=True) wsgiref.handlers.CGIHandler().run(application) if __name__ == "__main__": main()
paproxy.yaml(設定ファイル・'paproxy.py'と同一ディレクトリに設置)
※AWSAccessKeyId、SecretAccessKeyを自分のものに変更すること。
#============================================================================== # Mandatory Parameters #============================================================================== # === Your Access Key ID AWSAccessKeyId: 99999999999999999999 # === Your Secret Access Key SecretAccessKey: 1234567890123456789012345678901234567890 #============================================================================== # Optional Parameters #============================================================================== DefaultAssociateTag: furyutei-22 DefaultLocale: jp
参考
Amazon API認証のPROXYを書いたよ(AmazonのAPI認証導入はOSSに対する挑戦だよなぁ(4)) - ただのにっき(2009-06-19)
無題メモランダム: Amazon Product Advertising APIの署名認証をPythonでやってみる
http://developer.amazonwebservices.com/connect/ann.jspa?annID=442
Docs: Product Advertising API (Version: 2009-06-01) : Documentation Archive : Amazon Web Services
Product Advertising API
追記
http://www.karashi.org/~poppen/d/20090623.html#p01
覚書とか
- Product Advertising APIの仕様("/"はエンコード要、"~"は不要)にあわせると、urllib.quoteの引数にsafe='~'が必要っぽい。
- XSLTを使用する場合(Styleオプション指定時)、http://webservices.amazon.co.jp/onca/xmlやhttp://ecs.amazonaws.jp/onca/xmlで指定すると認証エラーに。専用のエンドポイント(http://xml-jp.amznxslt.com/onca/xml)の指定が必要らしい。*1
*1:Styleオプション無しのときはどれでもOKだったので、http://xml-jp.amznxslt.com/onca/xmlに統一してしまっても良さそうだが、一応わけてある。