diff options
author | Shawn O. Pearce <sop@google.com> | 2009-02-19 16:04:43 -0800 |
---|---|---|
committer | Shawn O. Pearce <sop@google.com> | 2009-02-23 11:09:26 -0800 |
commit | 4ee70f23058c2b1ced8b3891d5e91676639f3b3e (patch) | |
tree | 9d7a4f41913503f1a76506612cb672e138be04f2 | |
download | gerrit-contactstore-4ee70f23058c2b1ced8b3891d5e91676639f3b3e.tar.gz |
Receives encrypted contact information from Gerrit and stores it
into Google App Engine. Application developers can login through
the web UI, locate records, and save them locally for decryption.
Signed-off-by: Shawn O. Pearce <sop@google.com>
-rw-r--r-- | COPYING | 202 | ||||
-rwxr-xr-x | GIT-VERSION-GEN | 17 | ||||
-rw-r--r-- | README | 83 | ||||
-rw-r--r-- | google_appengine/.gitignore | 3 | ||||
-rw-r--r-- | google_appengine/Makefile | 54 | ||||
-rw-r--r-- | google_appengine/app.yaml | 27 | ||||
-rw-r--r-- | google_appengine/index.yaml | 24 | ||||
-rw-r--r-- | google_appengine/main.py | 102 | ||||
-rw-r--r-- | google_appengine/model.py | 23 | ||||
-rw-r--r-- | google_appengine/secure.py | 174 | ||||
-rw-r--r-- | google_appengine/static/application_version | 0 | ||||
-rw-r--r-- | google_appengine/static/robots.txt | 8 |
12 files changed, 717 insertions, 0 deletions
@@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN new file mode 100755 index 0000000..6126f62 --- /dev/null +++ b/GIT-VERSION-GEN @@ -0,0 +1,17 @@ +#!/bin/sh + +VN=$(git describe --abbrev=8 HEAD 2>/dev/null) +case "$VN" in +v[0-9]*) : happy ;; +*) exit 1 ;; +esac + +dirty=$(sh -c 'git diff-index --name-only HEAD' 2>/dev/null) || dirty= +case "$dirty" in +'') + ;; +*) + VN="$VN-dirty" ;; +esac + +echo $VN @@ -0,0 +1,83 @@ +gerrit-contactstore +=================== + +Utilities to receive contact information for individual users from +Gerrit and archive them in an encrypted store. + +Gerrit connects to the store by sending a standard HTTP POST request +to the store URL, with the following form parameters in the body: + +* APPSEC + + A shared secret "password" that should be known only to Gerrit + and the contact store. The contact store should test this value + to deter spamming of the contact store by outside parties. + +* account_id + + Unique account_id value from the Gerrit database for the account + the contact information belongs to. Base 10 integer. + +* email + + Preferred email address of the account. May facilitate lookups + in the contact store at a future date. May not be provided. + +* filed + + Seconds since the UNIX epoch of when the contact information + was filed. May be omitted or the empty string if the application + doesn't think the supplied contact information is valid enough. + +* data + + Encrypted account data as an armored ASCII blob. This is usually + several KB of text data as a single string, with embedded newlines + to break the lines at about 70-75 characters. Data can be decoded + using GnuPG with the correct private key. + +Using HTTPS for the store is encouraged, as it prevents +man-in-the-middle attacks at reading the shared secret +APPSEC token, or messing with the data packet. + +A successful store should respond with HTTP status code "200 OK" +and a text/plain content consisting of only "OK\n". Any other +response is considered to be a store failure. + + +Implementations +--------------- + +google_appengine/ +~~~~~~~~~~~~~~~~~ + +This implementation of the contact store runs on Google App Engine. + +It is a two very small Python CGIs: main.py receives the data and +secure.py permits some limited searching and retrieval of the data +by the application's owners (aka "developers" to Google App Engine). + +The encrypted data payload is stored as-is when received; that is +the data stays fully encrypted within AppEngine, and the private +key is never stored on AppEngine. It is therefore impossible for +Google to decipher or otherwise read the contact information stored. + +To use this implementation, sign up for a free AppEngine account, +then install the code with: + + cd google_appengine + make APPID=your-app-id-here update + +and configure Gerrit to use your new application instance: + + $ psql reviewdb + UPDATE system_config SET + contact_store_url = 'https://your-app-id-here.appspot.com/store' + ,contact_store_appsec = 'appsec-key-printed-during-update'; + +Later you can search for and download the encrypted contact +information by visiting your application over the web at +https://your-app-id-here.appspot.com/. Note that you must +sign-in with a developer account. This provides a very simple +access control system; to add additional users invite them to +be developers of your application. diff --git a/google_appengine/.gitignore b/google_appengine/.gitignore new file mode 100644 index 0000000..a438a9c --- /dev/null +++ b/google_appengine/.gitignore @@ -0,0 +1,3 @@ +*.pyc +/config.mak +/release diff --git a/google_appengine/Makefile b/google_appengine/Makefile new file mode 100644 index 0000000..9608a96 --- /dev/null +++ b/google_appengine/Makefile @@ -0,0 +1,54 @@ +# gerrit-contactstore for Google App Engine +# +# Define APPID to the unique Google App Engine application instance +# 'make update' will upload the application files to. +# +# Define APPSEC to the security token clients must present in order +# to upload to this application instance. Default is to generate +# a random string. +# +# Define APPCFG to the location of appcfg.py from the Google App +# Engine SDK download. +# + +APPID = gerrit-contactstore +APPSEC = $(shell sh -c 'dd if=/dev/urandom bs=128 count=1 2>/dev/null | md5sum | sed s/-//') +APPCFG = appcfg.py +CPIO = cpio -pd + +ifeq ($(shell uname),Darwin) + APPCFG = python /usr/local/bin/appcfg.py +endif + +-include config.mak + +R_WEB := release/web + +WEB_INCLUDE := $(strip \ + app.yaml \ + index.yaml \ + static \ + *.py \ +) + +release-web: + @echo Building gerrit-contactstore `../GIT-VERSION-GEN` for $(APPID): + @rm -rf $(R_WEB) + @mkdir -p $(R_WEB) + @echo " Copying loose files" && \ + find $(WEB_INCLUDE) -print | $(CPIO) $(abspath $(R_WEB)) + @../GIT-VERSION-GEN >$(R_WEB)/static/application_version + @perl -pi -e 's/(application:).*/$$1 $(APPID)/' $(R_WEB)/app.yaml + @a=$(APPSEC);echo "APPSEC='$$a'" >$(R_WEB)/appsec.py + @cat $(R_WEB)/appsec.py + @echo $(R_WEB) built for $(APPID). + +update: release-web + $(APPCFG) $(APPCFG_OPTS) update $(R_WEB) + +version: + @printf '%s = ' '$(APPID)' + @curl http://$(APPID).appspot.com/application_version + +clean: + @rm -rf release *.pyc diff --git a/google_appengine/app.yaml b/google_appengine/app.yaml new file mode 100644 index 0000000..b3a523b --- /dev/null +++ b/google_appengine/app.yaml @@ -0,0 +1,27 @@ +application: gerrit-contactstore +version: 1 +runtime: python +api_version: 1 +default_expiration: 7d + +handlers: +- url: /(robots.txt) + static_files: static/\1 + upload: static/robots.txt + secure: never + +- url: /(application_version) + static_files: static/\1 + mime_type: text/plain + expiration: 1s + upload: static/application_version + secure: never + +- url: /secure/.* + script: secure.py + login: admin + secure: always + +- url: .* + script: main.py + secure: always diff --git a/google_appengine/index.yaml b/google_appengine/index.yaml new file mode 100644 index 0000000..61cef18 --- /dev/null +++ b/google_appengine/index.yaml @@ -0,0 +1,24 @@ +indexes: + +- kind: ContactInfo + properties: + - name: account_id + - name: filed + direction: desc + - name: __key__ + +- kind: ContactInfo + properties: + - name: email + - name: account_id + - name: filed + direction: desc + - name: __key__ + +- kind: ContactInfo + properties: + - name: domain + - name: account_id + - name: filed + direction: desc + - name: __key__ diff --git a/google_appengine/main.py b/google_appengine/main.py new file mode 100644 index 0000000..63f92c3 --- /dev/null +++ b/google_appengine/main.py @@ -0,0 +1,102 @@ +# Copyright 2009 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. + +from datetime import datetime +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import run_wsgi_app + +from appsec import APPSEC +from model import ContactInfo + +def _CreateApplication(): + return webapp.WSGIApplication([ + (r'^/store', StoreContact), + (r'^/', GoSecure), + (r'^/.*', PageNotFound), + ], + debug=False) + +class StoreContact(webapp.RequestHandler): + def get(self): + self.post() + + def post(self): + self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' + ci = ContactInfo() + try: + if APPSEC != self.request.get('APPSEC'): + raise ValueError + + ci.account_id = int(self.request.get('account_id')) + if ci.account_id < 1: + raise ValueError + + d = self.request.get('data') + if d is None or len(d) < 1: + raise ValueError + ci.data = db.Blob(d.encode('utf-8')) + + ci.email = self.request.get('email') + if ci.email is None or len(ci.email) == 0: + ci.email = None + ci.domain = None + else: + ci.email = ci.email.lower() + ci.domain = ci.email[ci.email.index('@') + 1:] + + f = self.request.get('filed') + if f is None or len(f) == 0: + f = datetime.utcnow() + else: + f = datetime.utcfromtimestamp(int(f)) + ci.filed = f + except: + self.response.set_status(500) + self.response.out.write('BAD INPUT\n') + raise + + try: + ci.put() + except: + self.response.set_status(500) + self.response.out.write('DATA STORE FAIL\n') + raise + + self.response.set_status(200) + self.response.out.write('OK\n') + +class GoSecure(webapp.RequestHandler): + def get(self): + self.redirect('/secure/', permanent=True) + +class PageNotFound(webapp.RequestHandler): + def get(self): + self.response.set_status(404) + self.response.headers['Content-Type'] = 'text/html; charset=utf-8' + self.response.out.write("""<html> +<head> + <title>Not Found</title> +</head> +<body> +<h1>Not Found</h1> +</html> +""") + +def main(): + run_wsgi_app(application) + +if __name__ == '__main__': + application = _CreateApplication() + main() diff --git a/google_appengine/model.py b/google_appengine/model.py new file mode 100644 index 0000000..2a49c84 --- /dev/null +++ b/google_appengine/model.py @@ -0,0 +1,23 @@ +# Copyright 2009 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. + +from google.appengine.ext import db + +class ContactInfo(db.Model): + account_id = db.IntegerProperty() + stored = db.DateTimeProperty(auto_now_add=True) + filed = db.DateTimeProperty() + email = db.StringProperty() + domain = db.StringProperty() + data = db.BlobProperty() diff --git a/google_appengine/secure.py b/google_appengine/secure.py new file mode 100644 index 0000000..3bd8d3d --- /dev/null +++ b/google_appengine/secure.py @@ -0,0 +1,174 @@ +# Copyright 2009 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 cgi +from google.appengine.ext import db +from google.appengine.ext import webapp +from google.appengine.ext.webapp.util import run_wsgi_app + +from model import ContactInfo + +def _CreateApplication(): + return webapp.WSGIApplication([ + (r'^/secure/$', ShowForm), + (r'^/secure/account_id$', QueryAccountId), + (r'^/secure/email$', QueryEmail), + (r'^/secure/domain$', QueryDomain), + (r'^/secure/show$', ShowData), + ], + debug=False) + +def esc(s): + if s is None: + return '' + return cgi.escape(s) + +def td(out, s): + if s is None: + s = ' ' + else: + s = esc(str(s)) + out.write('<td>') + out.write(s) + out.write('</td>') + +class ShowForm(webapp.RequestHandler): + def get(self): + self.response.set_status(200) + self.response.headers['Content-Type'] = 'text/html; charset=utf-8' + self.response.out.write("""<html> +<head> + <title>Query</title> +</head> +<body> +<h1>Query</h1> + +<div> + <form action="account_id" method="POST"> + <b>Account ID:</b> + <input type="text" name="q" size="25" /> + <input type="submit" value="Search" /> + </form> +</div> + +<div> + <form action="email" method="POST"> + <b>Email Address:</b> + <input type="text" name="q" size="25" /> + <input type="submit" value="Search" /> + </form> +</div> + +<div> + <form action="domain" method="POST"> + <b>Domain:</b> + @<input type="text" name="q" size="25" /> + <input type="submit" value="Search" /> + </form> +</div> + +</html> +""") + +class QueryBase(webapp.RequestHandler): + def post(self): + q = self.request.get('q').lower() + if q in ('', 'null', 'none'): + q = None + + self.response.set_status(200) + self.response.headers['Content-Type'] = 'text/html; charset=utf-8' + self.response.out.write("""<html> +<head> + <title>Query</title> +</head> +<body> +<h1>Query '%s'</h1> +<table border="1"> +<tr> + <th>Account</th> + <th>Email</th> + <th>Filed</th> + <th>Stored</th> +</tr> +""" % (esc(q))) + + found = list(self.query_for(q)) + for ci in found: + self.response.out.write('<tr>') + self.response.out.write('<td align="right"><a href="show?key=%s">%d</a></td>' + % (esc(str(ci.key())), ci.account_id)) + td(self.response.out, ci.email) + td(self.response.out, ci.filed) + td(self.response.out, ci.stored) + self.response.out.write('</tr>\n') + + self.response.out.write("""</table> +%d matches found. +</body> +</html> +""" % len(found)) + +class QueryAccountId(QueryBase): + def query_for(self, q): + return ContactInfo.gql( + 'WHERE account_id = :1 ORDER BY filed DESC, __key__', + int(q) + ) + +class QueryEmail(QueryBase): + def query_for(self, q): + return ContactInfo.gql( + 'WHERE email = :1 ORDER BY account_id, filed DESC, __key__', + q + ) + +class QueryDomain(QueryBase): + def query_for(self, q): + return ContactInfo.gql( + 'WHERE domain = :1 ORDER BY account_id, filed DESC, __key__', + q + ) + +class ShowData(webapp.RequestHandler): + def get(self): + try: + key = db.Key(self.request.get('key')) + except: + self.response.set_status(404) + self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' + self.response.out.write('Not Found\n') + return + + ci = ContactInfo.get(key) + if ci is None: + self.response.set_status(404) + self.response.headers['Content-Type'] = 'text/plain; charset=utf-8' + self.response.out.write('Not Found\n') + else: + self.response.set_status(200) + self.response.headers['Content-Type'] \ + = 'application/octet-stream' + self.response.headers['Content-Length'] \ + = len(ci.data) + self.response.headers['Content-Disposition'] \ + = 'inline; filename="account_%d.enc"' % ci.account_id + self.response.out.write(ci.data) + +def main(): + run_wsgi_app(application) + +if __name__ == '__main__': + application = _CreateApplication() + main() diff --git a/google_appengine/static/application_version b/google_appengine/static/application_version new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/google_appengine/static/application_version diff --git a/google_appengine/static/robots.txt b/google_appengine/static/robots.txt new file mode 100644 index 0000000..c033917 --- /dev/null +++ b/google_appengine/static/robots.txt @@ -0,0 +1,8 @@ +# Directions for web crawlers. +# See http://www.robotstxt.org/wc/norobots.html. + +User-agent: HTTrack +User-agent: puf +User-agent: MSIECrawler +User-agent: Nutch +Disallow: / |