├── .gitignore ├── Guardfile ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── TODO.md ├── app.template.yaml ├── app.yaml ├── appengine_config.py ├── appengine_config.template.py ├── client ├── cron.yaml ├── cron.yaml.template ├── index.yaml ├── queue.yaml ├── searchable.json ├── searchable.template.json ├── tailbone ├── .gitignore ├── __init__.py ├── admin │ ├── __init__.py │ ├── abuse.html │ └── include.yaml ├── authentication.js ├── clocksync │ ├── __init__.py │ ├── clocksync.js │ └── include.yaml ├── cloudstore │ ├── __init__.py │ └── include.yaml ├── compute_engine │ ├── __init__.py │ ├── admin.html │ └── include.yaml ├── customce │ ├── __init__.py │ └── include.yaml ├── dependencies.zip ├── files │ ├── __init__.py │ └── include.yaml ├── geoip │ ├── __init__.py │ └── include.yaml ├── globals.js ├── include.yaml ├── mesh │ ├── __init__.py │ ├── channel │ │ ├── README.md │ │ ├── __init__.py │ │ └── include.yaml │ ├── include.yaml │ ├── js │ │ ├── Channel.js │ │ ├── ChannelChannel.js │ │ ├── ChannelMultiplexer.js │ │ ├── EventDispatcher.js │ │ ├── Mesh.js │ │ ├── NetChannel.js │ │ ├── Node.js │ │ ├── Peers.js │ │ ├── RTCChannel.js │ │ ├── SocketChannel.js │ │ ├── SocketMultiplexer.js │ │ ├── StateDrive.js │ │ └── msgpack.js │ ├── websocket.js │ └── websocket.py ├── pathrewrite │ ├── __init__.py │ ├── include.yaml │ └── index.html ├── proxy │ ├── __init__.py │ └── include.yaml ├── restful │ ├── __init__.py │ ├── counter.py │ ├── include.yaml │ └── models.js ├── search │ ├── __init__.py │ └── include.yaml ├── static │ ├── __init__.py │ ├── protected │ │ ├── __init__.py │ │ └── include.yaml │ └── public │ │ ├── __init__.py │ │ └── include.yaml ├── test │ ├── __init__.py │ ├── auth.html │ ├── clocksync.html │ ├── events.html │ ├── extras │ │ ├── jquery.min.js │ │ ├── qunit-git.css │ │ └── qunit-git.js │ ├── files.html │ ├── include.yaml │ ├── mesh.html │ ├── messages.html │ ├── metadata.html │ ├── proxy.html │ ├── restful.html │ └── search.html └── turn │ ├── __init__.py │ └── include.yaml ├── validation.json └── validation.template.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | Gemfile.lock 4 | .sass-cache 5 | *.idea 6 | credentials.json 7 | *.p12 8 | *.pem 9 | tailbone/mesh/node_modules 10 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Guardfile 4 | # More info at https://github.com/lepture/python-livereload 5 | 6 | from livereload.task import Task 7 | from livereload.compiler import shell 8 | 9 | 10 | def recursive_watch(directory, filetypes, *args, **kwargs): 11 | import os 12 | for root, dirs, files in os.walk(directory): 13 | if filetypes: 14 | towatch = set() 15 | for filetype in filetypes: 16 | for f in files: 17 | if filetype in f: 18 | towatch.add(filetype) 19 | for filetype in towatch: 20 | Task.add(os.path.join(root,"*.{}".format(filetype)), *args, **kwargs) 21 | else: 22 | Task.add(os.path.join(root, "*"), *args, **kwargs) 23 | 24 | 25 | recursive_watch("client/app", []) 26 | recursive_watch("tailbone", ["py", "html", "js", "css", "yaml"]) 27 | 28 | recursive_watch("client/app", ["scss"], shell('sass --update', 'client/app')) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | v1.0.0 2 | ----------- 3 | 4 | * Use of tailbone as a submodule 5 | * New libraries for compute engine 6 | * appengine_config.py for configuration 7 | 8 | 9 | v0.1.0 10 | ----------- 11 | 12 | * Baseline release 13 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ---- 3 | 4 | - Create a port of tailbone to the [endpoints 5 | api](https://developers.google.com/appengine/docs/python/endpoints/) 6 | - Rethink how access control is done, possibly with admin defined validation snippets in javascript or python or 7 | regex acting upon an entire class. 8 | - Possibly give option for tighter integration between the events api and the datastore api -------------------------------------------------------------------------------- /app.template.yaml: -------------------------------------------------------------------------------- 1 | application: your-application-id 2 | version: master 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | ## Turn on and off modules or add your own by modifying the includes 8 | ## Make sure one of the tailbone/static includes are at the end 9 | ## Usually easiest to add your specific includes to the begining of the includes list 10 | 11 | includes: 12 | ## Base includes, authentication, and combined /tailbone.js file 13 | - tailbone 14 | ## Qunit tests (only works when running locally) 15 | - tailbone/test 16 | ## Get the user location 17 | # - tailbone/geoip 18 | ## Full text search must provide a searchable.json at the root level 19 | # - tailbone/search 20 | ## Storing and fetching files with the blobstore api 21 | # - tailbone/files 22 | ## Read access to any of your cloud storage objects. 23 | # - tailbone/cloudstore 24 | ## Simple reverse proxy sometimes useful for cors purposes (note: don't use if you don't have to it 25 | ## will cost you bandwidth and has no auth you can domain restrict in appengine_config.py) 26 | # - tailbone/proxy 27 | ## Clocksync simple synchronization of clocks 28 | # - tailbone/clocksync 29 | ## Websocket+WebRTC based mesh server with rooms and load balancing 30 | # - tailbone/mesh 31 | ## Load balanced stand alone turn server service 32 | # - tailbone/turn 33 | ## Run custom load balanced server on compute engine {eg node.js websocket server} 34 | # - tailbone/customce 35 | ## Order in this list is important for restful and pathrewrite, they must be last. 36 | ## Restful resources 37 | - tailbone/restful 38 | ## Any path without a file extension not in /api gets the app/client/index.html text. Useful for 39 | ## html5 applications that use the history api to write their paths. 40 | # - tailbone/pathrewrite 41 | ## Serve the app folder. Static protected resource can change protection in appengine_config.py 42 | # - tailbone/static/protected 43 | ## Serve the app folder. Static public resource (can only have protected or public not both) 44 | - tailbone/static/public 45 | 46 | 47 | ## Don't edit below this line unless before reading about https://developers.google.com/appengine/docs/python/config/appconfig 48 | 49 | builtins: 50 | - appstats: on 51 | - deferred: on 52 | - admin_redirect: on 53 | - remote_api: on 54 | 55 | inbound_services: 56 | - warmup 57 | - channel_presence 58 | 59 | skip_files: 60 | - ^(.*/)?index\.yaml 61 | - ^(.*/)?index\.yml 62 | - ^(.*/)?#.*# 63 | - ^(.*/)?.*~ 64 | - ^(.*/)?.*\.py[co] 65 | - ^(.*/)?.*/RCS/.* 66 | - ^(.*/)?\..* 67 | - ^client/tailbone/.* 68 | 69 | pagespeed: 70 | # domains_to_rewrite: 71 | # - slowdomain.com 72 | # url_blacklist: 73 | # - http://myapp.com/dontoptomize/* 74 | enabled_rewriters: 75 | - MinifyCss 76 | disabled_rewriters: 77 | - LazyloadImages 78 | # Available rewriters 79 | # - ProxyCss # - default 80 | # - ProxyImages # - default 81 | # - ProxyJs # - default 82 | # - ConvertMetaTags # - default 83 | # - InlineCss # - default 84 | # - InlineJs 85 | # - InlineImages 86 | # - InlinePreviewImages # - default 87 | # - CollapseWhitespace 88 | # - CombineHeads 89 | # - ElideAttributes 90 | # - RemoveComments 91 | # - RemoveQuotes 92 | # - LeftTrimUrls 93 | 94 | # - CombineCss # - default 95 | # - MoveCssToHead # - default 96 | # - MinifyCss 97 | 98 | # - WebpOptimization # - default 99 | # - ImageConvertToJpeg # - default 100 | # - ImageRecompressJpeg # - default 101 | # - ImageProgressiveJpeg # - default 102 | # - ImageRecompressPng # - default 103 | # - ImageStripMetadata # - default 104 | # - ImageStripColorProfile # - default 105 | # - ImageResize # - default 106 | # - LazyloadImages # - default 107 | # - ImageAddDimensions 108 | 109 | # - CombineJs # - default 110 | # - JsOptimize # - default 111 | # - DeferJs 112 | 113 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | ../app.yaml -------------------------------------------------------------------------------- /appengine_config.py: -------------------------------------------------------------------------------- 1 | ../appengine_config.py -------------------------------------------------------------------------------- /appengine_config.template.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | ## Edit the code below to add you own hooks and modify tailbone's behavior 17 | 18 | ## Base Tailbone overrides and hooks 19 | 20 | ## Set the global default namespace 21 | # def namespace_manager_default_namespace_for_request(): 22 | # return "my_custom_namespace" 23 | 24 | ## Use JSONP for all apis 25 | # tailbone_JSONP = False 26 | 27 | ## Use CORS for all apis 28 | # tailbone_CORS = True 29 | # tailbone_CORS_RESTRICTED_DOMAINS = ["http://localhost"] 30 | 31 | ## modify the below functions to change how users are identified 32 | # tailbone_is_current_user_admin = 33 | # tailbone_get_current_user = 34 | # tailbone_create_login_url = 35 | # tailbone_create_logout_url = 36 | ## Example of allowing anybody to write to the datastore by always using a fake user 37 | # class FakeUser(object): 38 | # def user_id(self): 39 | # return "fakeuser" 40 | # u = FakeUser() 41 | # def fake_user(): 42 | # return u 43 | # tailbone_get_current_user = fake_user 44 | 45 | ## Use google plus for login and identity 46 | #CLIENT_ID = '' 47 | #CLIENT_SECRET = '' 48 | 49 | #def gplusFriends(gplus, http, token=None): 50 | #result = gplus.people().list(userId='me', collection='visible', pageToken=token, orderBy='best').execute(http=http) 51 | #token = result.pop('nextPageToken', None) 52 | #if token: 53 | #result['items'] += gplusFriends(gplus, http, token).get('items', []) 54 | #return result 55 | 56 | 57 | #class GPlusUser(object): 58 | #def __init__(self, user_id=None, credentials=None): 59 | #self.__user_id = user_id 60 | #self.__credentials = credentials 61 | #def user_id(self): 62 | #return self.__user_id 63 | #def friends(self): 64 | #import httplib2 65 | #http = httplib2.Http() 66 | #http = self.__credentials.authorize(http) 67 | #from apiclient.discovery import build 68 | #gplus = build('plus', 'v1') 69 | #return gplusFriends(gplus, http) 70 | #def profile(self): 71 | #import httplib2 72 | #http = httplib2.Http() 73 | #http = self.__credentials.authorize(http) 74 | #from apiclient.discovery import build 75 | #gplus = build('plus', 'v1') 76 | ## fetch me 77 | #result = gplus.people().get(userId='me').execute(http=http) 78 | #return result 79 | 80 | #import webapp2 81 | #from webapp2_extras import sessions 82 | #def get_current_user(*args, **kwargs): 83 | ## get webapp2 84 | ## get session 85 | ## get user from session 86 | #request = webapp2.get_request() 87 | #session_store = sessions.get_store(request=request) 88 | #session = session_store.get_session() 89 | #credentials = session.get('credentials', None) 90 | #if credentials: 91 | #from oauth2client.client import OAuth2Credentials 92 | #credentials = OAuth2Credentials.from_json(credentials) 93 | #user_id = session.get('gplus_id', None) 94 | #user = GPlusUser(user_id, credentials) 95 | #return user 96 | #return None 97 | 98 | #tailbone_get_current_user = get_current_user 99 | 100 | #scope = 'https://www.googleapis.com/auth/plus.login' 101 | #redirect_path = '/api/login' 102 | 103 | #def secret(): 104 | #import random 105 | #import string 106 | #return ''.join(random.choice(string.ascii_uppercase + string.digits) for x in xrange(32)) 107 | 108 | #def create_login_url(dest_url=None, **kwargs): 109 | #request = webapp2.get_request() 110 | #session_store = sessions.get_store(request=request) 111 | #session = session_store.get_session() 112 | #state = { 113 | #'secret': session.get('secret') 114 | #} 115 | #if dest_url: 116 | #state['continue'] = dest_url 117 | #import json 118 | #state = json.dumps(state) 119 | #redirect_uri = request.host_url + redirect_path 120 | #url = 'https://accounts.google.com/o/oauth2/auth?\ 121 | #scope={}&\ 122 | #state={}&\ 123 | #redirect_uri={}&\ 124 | #response_type=code&\ 125 | #client_id={}&\ 126 | #access_type=offline'.format(scope, state, redirect_uri, CLIENT_ID) 127 | #return url 128 | 129 | #tailbone_create_login_url = create_login_url 130 | 131 | #def create_logout_url(dest_url): 132 | #return dest_url 133 | 134 | #tailbone_create_logout_url = create_logout_url 135 | 136 | #def destroy_user(self): 137 | ##clear the local session and call the revoke_uri 138 | #request = webapp2.get_request() 139 | #session_store = sessions.get_store(request=request) 140 | #session = session_store.get_session() 141 | #credentials = session.pop('credentials', None) 142 | #gplus_id = session.pop('gplus_id', None) 143 | #if credentials: 144 | #from oauth2client.client import OAuth2Credentials 145 | #credentials = OAuth2Credentials.from_json(credentials) 146 | #revoke_uri = str("%s?token=%s" % (credentials.revoke_uri, credentials.access_token)) 147 | #from google.appengine.api import urlfetch 148 | #urlfetch.fetch(revoke_uri) 149 | #session_store.save_sessions(self.response) 150 | 151 | #tailbone_logout_hook = destroy_user 152 | 153 | #def create_user(self): 154 | #session_store = sessions.get_store(request=self.request) 155 | #session = session_store.get_session() 156 | 157 | #state = self.request.get('state') 158 | #if state: 159 | #import json 160 | #state = json.loads(state) 161 | #if state.get('secret', False) != session.get('secret', True): 162 | #raise Exception('Invalid secret.') 163 | 164 | #code = self.request.get('code') 165 | #from oauth2client.client import credentials_from_code 166 | #redirect_uri = request.host_url + redirect_path 167 | #credentials = credentials_from_code(CLIENT_ID, CLIENT_SECRET, scope, code, 168 | #redirect_uri=redirect_uri) 169 | 170 | #session['credentials'] = credentials.to_json() 171 | #session['gplus_id'] = credentials.id_token['sub'] 172 | #session_store.save_sessions(self.response) 173 | 174 | #redirect = state.get('continue', None) 175 | #if redirect: 176 | #self.redirect(redirect) 177 | #return True 178 | #else: 179 | #session['secret'] = secret() 180 | #session_store.save_sessions(self.response) 181 | 182 | #tailbone_login_hook = create_user 183 | 184 | #tailbone_CONFIG = { 185 | #'webapp2_extras.sessions': { 186 | #'secret_key': 'my-super-secret-key' 187 | #} 188 | #} 189 | 190 | 191 | ## Use cloud store instead of blobstore 192 | # tailboneFiles_CLOUDSTORE = False 193 | 194 | ## Store counts for restful models accessible in HEAD query 195 | # tailboneRestful_METADATA = False 196 | 197 | ## If specified is a list of tailbone.restful.ScopedModel objects these will be the only ones allowed. 198 | ## This is a next level step of model restriction to your db, this replaces validation.json 199 | # from google.appengine.ext import ndb 200 | # from tailbone.restful import ScopedModel 201 | # class MyModel(ScopedModel): 202 | # stuff = ndb.IntegerProperty() 203 | # tailboneRestful_DEFINED_MODELS = {"mymodel": MyModel} 204 | # tailboneRestful_RESTRICT_TO_DEFINED_MODELS = False 205 | 206 | ## Protected model names gets overridden by RESTRICTED_MODELS 207 | # tailboneRestful_PROTECTED_MODEL_NAMES = ["(?i)tailbone.*", "custom", "(?i)users"] 208 | 209 | ## Proxy can only be used for the restricted domains if specified 210 | # tailboneProxy_RESTRICTED_DOMAINS = ["google.com"] 211 | 212 | ## Cloud store bucket to use default is your application id 213 | # tailboneCloudstore_BUCKET = "mybucketname" 214 | 215 | # tailboneTurn_RESTIRCTED_DOMAINS = ["localhost"] 216 | # tailboneTurn_SECRET = "notasecret" 217 | 218 | # tailboneMesh_ENABLE_TURN = True 219 | # tailboneMesh_ENABLE_WEBSOCKET = True 220 | 221 | ## Seconds until room expires 222 | # tailboneMesh_ROOM_EXPIRATION = 86400 223 | 224 | ## Protected site 225 | # tailboneStaticProtected_PASSWORD = "mypassword" 226 | ## the base path for the protected site can change to deploy or something else defaults to app 227 | # tailboneStaticProtected_BASE_PATH = "app" 228 | 229 | ## Custom load balanced compute engine instance 230 | # tailboneCustomCE_STARTUP_SCRIPT = """ 231 | # apt-get install build-essential 232 | 233 | # curl -O http://nodejs.org/dist/v0.10.15/node-v0.10.15.tar.gz 234 | # tar xvfz node-v0.10.15.tar.gz 235 | # cd node-v0.10.15 236 | # ./configure 237 | # make 238 | # make install 239 | # cd .. 240 | # rm -rf node-v0.10.15 241 | # rm -f node-v0.10.15.tar.gz 242 | 243 | # cat >server.js < 316 | 317 | 318 | 319 | 322 | 323 | 324 | If this window should have been automatically closed, you may not have javascript enabled. 325 | 330 | 331 | """) 332 | 333 | 334 | EXPORTED_JAVASCRIPT = compile_js([ 335 | "tailbone/authentication.js" 336 | ], ["login", "logout", "login_url", "logout_url", "authorized"]) 337 | 338 | auth = webapp2.WSGIApplication([ 339 | (r"{}login".format(PREFIX), LoginHandler), 340 | (r"{}logout" .format(PREFIX), LogoutHandler), 341 | (r"{}logup".format(PREFIX), LoginHelperHandler), 342 | ], debug=DEBUG, config=config.CONFIG) 343 | 344 | app = webapp2.WSGIApplication([ 345 | (r"/tailbone.js", JsHandler), 346 | ], debug=DEBUG, config=config.CONFIG) 347 | 348 | class AddSlashHandler(webapp2.RequestHandler): 349 | def get(self): 350 | url = self.request.path + "/" 351 | if self.request.query_string: 352 | url += "?" + self.request.query_string 353 | self.redirect(url) 354 | 355 | add_slash = webapp2.WSGIApplication([ 356 | (r".*", AddSlashHandler) 357 | ], debug=DEBUG, config=config.CONFIG) 358 | -------------------------------------------------------------------------------- /tailbone/admin/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # shared resources and global variables 16 | from tailbone import * 17 | 18 | def ban(self): 19 | return {} 20 | 21 | def notFound(self): 22 | self.error(404) 23 | return {"error": "Not Found"} 24 | 25 | class AdminShortcutHandler(BaseHandler): 26 | @as_json 27 | def get(self, action): 28 | return { 29 | "ban": ban 30 | }.get(action, notFound)(self) 31 | 32 | app = webapp2.WSGIApplication([ 33 | (r"{}admin/(.*)".format(PREFIX), AdminShortcutHandler), 34 | ], debug=DEBUG) 35 | -------------------------------------------------------------------------------- /tailbone/admin/abuse.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Block/Unblock User: 8 |
9 | 10 | 11 |
12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tailbone/admin/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/admin/?.* 17 | script: tailbone.admin.app 18 | login: admin 19 | - url: /admin/abuse 20 | static_files: tailbone/admin/abuse.html 21 | upload: tailbone/admin/abuse.html 22 | login: admin 23 | 24 | admin_console: 25 | pages: 26 | - name: Datastore Abuse 27 | url: /admin/abuse 28 | 29 | -------------------------------------------------------------------------------- /tailbone/authentication.js: -------------------------------------------------------------------------------- 1 | function processResponse(callback) { 2 | var pt = function(message) { 3 | window.removeEventListener('message', pt); 4 | var localhost = false; 5 | if (message.origin.match("localhost")) { 6 | localhost = true; 7 | } 8 | if (!localhost && message.origin !== ( window.location.protocol + "//" + window.location.host )) { 9 | throw new Error('Origin does not match.'); 10 | } else { 11 | if (typeof callback === 'function') { 12 | callback(message); 13 | } 14 | } 15 | }; 16 | return pt; 17 | } 18 | 19 | function login(callback) { 20 | var options = Array.prototype.slice.apply(arguments); 21 | options = options.slice(1) || []; 22 | window.addEventListener('message', processResponse(callback), false); 23 | options.unshift('/api/login?continue=/api/logup'); 24 | window.open.apply(window, options); 25 | }; 26 | 27 | function logout(callback) { 28 | authorized.user = undefined; 29 | http.GET('/api/logout?continue=/api/logup', callback); 30 | }; 31 | 32 | function authorized(callback) { 33 | if (typeof callback !== 'function') { 34 | if (window.console) { 35 | console.warn('authorize called without callback defined.'); 36 | } 37 | return; 38 | } 39 | var options = Array.prototype.slice.apply(arguments); 40 | options = options.slice(1) || []; 41 | if (authorized.user) { 42 | callback(authorized.user); 43 | } 44 | http.GET('/api/users/me', function(user) { 45 | authorized.user = user; 46 | callback(authorized.user); 47 | }, function() { 48 | options.unshift(function() { 49 | http.GET('/api/users/me', function(user) { 50 | authorized.user = user; 51 | callback(authorized.user); 52 | }); 53 | }); 54 | login.apply(this, options); 55 | }); 56 | }; 57 | 58 | // Constructs a login url. 59 | function logout_url(redirect_url) { 60 | return '/api/login?url=' + (redirect_url || '/'); 61 | }; 62 | 63 | function login_url(redirect_url) { 64 | return '/api/login?url=' + (redirect_url || '/'); 65 | }; -------------------------------------------------------------------------------- /tailbone/clocksync/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tailbone import DEBUG, PREFIX, compile_js, as_json, BaseHandler 16 | 17 | import time 18 | import webapp2 19 | 20 | 21 | class ClockSyncHandler(BaseHandler): 22 | def head(self): 23 | self.response.headers['Last-Modified'] = "{:f}".format(time.time()*1000) 24 | 25 | @as_json 26 | def get(self): 27 | return time.time()*1000 28 | 29 | EXPORTED_JAVASCRIPT = compile_js([ 30 | "tailbone/clocksync/clocksync.js", 31 | ], ["clocksync"]) 32 | 33 | app = webapp2.WSGIApplication([ 34 | (r"{}clocksync/?.*".format(PREFIX), ClockSyncHandler), 35 | ], debug=DEBUG) 36 | -------------------------------------------------------------------------------- /tailbone/clocksync/clocksync.js: -------------------------------------------------------------------------------- 1 | //https://github.com/ffdead/clocksync 2 | 3 | // Simple Time sync helper class 4 | 5 | /* 6 | 7 | Time synchronization protocol for networked clients 8 | 9 | Example usage: 10 | 11 | // setup a sync method using ajax (or websockets etc) 12 | // use clocksync.data() to generate the data needed to send to the server 13 | // handle server response to clocksync.syncResponse 14 | clocksync.syncMethod = function () { 15 | $.post('/sync', clocksync.data(), clocksync.syncResponse, 'text'); 16 | } 17 | 18 | // sync 19 | clocksync.sync(function (now, delta) { 20 | console.log('obtained synched time:', now, ' local delta: ', delta); 21 | }); 22 | 23 | 24 | Algorithm (http://www.mine-control.com/zack/timesync/timesync.html): 25 | 1. Client stamps current local time on a "time request" packet and sends to 26 | server 27 | 2. Upon receipt by server, server stamps server-time and returns 28 | 3. Upon receipt by client, 29 | 4. The first result should immediately be used to update the clock since it 30 | will get the local clock into at least the right ballpark (at least the 31 | right timezone!) 32 | 5. The client repeats steps 1 through 3 five or more times, pausing a few 33 | seconds each time. Other traffic may be allowed in the interim, but 34 | should be minimized for best results 35 | 6. The results of the packet receipts are accumulated and sorted in lowest 36 | -latency to highest-latency order. The median latency is determined by 37 | picking the mid-point sample from this ordered list. 38 | 7. All samples above approximately 1 standard-deviation from the median are 39 | discarded and the remaining samples are averaged using an arithmetic mean. 40 | */ 41 | 42 | var samples = [], 43 | clockDelta = 0; 44 | 45 | var now; 46 | if (window.performance && performance.now) { 47 | now = function() { 48 | return performance.timing.navigationStart + performance.now(); 49 | }; 50 | } else { 51 | now = function() { 52 | return +new Date(); 53 | }; 54 | } 55 | 56 | var nowsync = function () { 57 | return now() - clockDelta; 58 | }; 59 | 60 | var createSyncData = function () { 61 | return {clientLocalTime: now()}; 62 | }; 63 | 64 | var handleSyncResponse = function (data) { 65 | var receiveTime = now(); 66 | if (typeof(data) === 'string') { 67 | data = JSON.parse(data); 68 | } 69 | data.receiveTime = receiveTime; 70 | data.latency = (data.receiveTime - data.clientLocalTime) * 0.5; 71 | data.clockDelta = data.receiveTime - data.serverLocalTime - data.latency; 72 | updateSync(data); 73 | }; 74 | 75 | var updateSync = function (sample) { 76 | clockDelta = sample.clockDelta; 77 | pushSample(sample); 78 | if (samples.length < 9) { 79 | return setTimeout(clocksync.syncMethod, 100); 80 | } 81 | completeSync(); 82 | }; 83 | 84 | var completeSync = function () { 85 | var list = filterSamples(samples); 86 | clockDelta = getAverageClockDelta(list); 87 | if (clocksync.callback) { 88 | clocksync.callback(nowsync(), clockDelta); 89 | } 90 | }; 91 | 92 | var pushSample = function (sample) { 93 | var i, len = samples.length; 94 | 95 | for(i = 0; i < len; i++) { 96 | if (sample.latency < samples[i].latency) { 97 | samples.splice(i, 0, sample); 98 | return; 99 | } 100 | } 101 | samples.push(sample); 102 | }; 103 | 104 | var calcMedian = function (values) { 105 | var half = Math.floor(values.length/2); 106 | if(values.length % 2) { 107 | return values[half]; 108 | } else { 109 | return (values[half-1] + values[half]) / 2.0; 110 | } 111 | }; 112 | 113 | var filterSamples = function (samples) { 114 | var list = [], 115 | i, 116 | latency, 117 | len = samples.length, 118 | sd = getStandardDeviation(samples), 119 | median = calcMedian(samples); 120 | 121 | for (i=0; i < len; i++) { 122 | latency = samples[i].latency; 123 | if (latency > median.latency - sd && latency < median.latency + sd) { 124 | list.push(samples[i]); 125 | } 126 | } 127 | 128 | return list; 129 | }; 130 | 131 | var getAverageClockDelta = function (samples) { 132 | var i = samples.length, 133 | sum = 0; 134 | while( i-- ){ 135 | sum += samples[i].clockDelta; 136 | } 137 | return sum / samples.length; 138 | }; 139 | 140 | 141 | 142 | // http://bateru.com/news/2011/03/javascript-standard-deviation-variance-average-functions/ 143 | var isArray = function (obj) { 144 | return Object.prototype.toString.call(obj) === "[object Array]"; 145 | }, 146 | getNumWithSetDec = function( num, numOfDec ){ 147 | var pow10s = Math.pow( 10, numOfDec || 0 ); 148 | return ( numOfDec ) ? Math.round( pow10s * num ) / pow10s : num; 149 | }, 150 | getAverageFromNumArr = function( numArr, numOfDec ){ 151 | if( !isArray( numArr ) ){ return false; } 152 | var i = numArr.length, 153 | sum = 0; 154 | while( i-- ){ 155 | sum += numArr[ i ].latency; 156 | } 157 | return getNumWithSetDec( (sum / numArr.length ), numOfDec ); 158 | }, 159 | getVariance = function( numArr, numOfDec ){ 160 | if( !isArray(numArr) ){ return false; } 161 | var avg = getAverageFromNumArr( numArr, numOfDec ), 162 | i = numArr.length, 163 | v = 0; 164 | 165 | while( i-- ){ 166 | v += Math.pow( (numArr[ i ].latency - avg), 2 ); 167 | } 168 | v /= numArr.length; 169 | return getNumWithSetDec( v, numOfDec ); 170 | }, 171 | getStandardDeviation = function( numArr, numOfDec ){ 172 | if( !isArray(numArr) ){ return false; } 173 | var stdDev = Math.sqrt( getVariance( numArr, numOfDec ) ); 174 | return getNumWithSetDec( stdDev, numOfDec ); 175 | }; 176 | 177 | 178 | var ajaxSyncMethod = function() { 179 | var xhr = new XMLHttpRequest(); 180 | xhr.open('HEAD', '/api/clocksync', false); 181 | var start = Date.now(); 182 | xhr.onreadystatechange = function(e) { 183 | if (xhr.readyState === xhr.DONE) { 184 | var server_time = parseFloat(xhr.getResponseHeader('Last-Modified')); 185 | clocksync.syncResponse({ 186 | clientLocalTime: start, 187 | serverLocalTime: server_time 188 | }); 189 | } 190 | }; 191 | xhr.send(); 192 | }; 193 | 194 | var clocksync = { 195 | delta: clockDelta, 196 | time: nowsync, 197 | sync: function(callback) { 198 | this.callback = callback; 199 | this.syncMethod(); 200 | }, 201 | syncMethod: ajaxSyncMethod, 202 | data: createSyncData, 203 | syncResponse: handleSyncResponse 204 | }; 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /tailbone/clocksync/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/clocksync/?.* 17 | script: tailbone.clocksync.app 18 | -------------------------------------------------------------------------------- /tailbone/cloudstore/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import webapp2 17 | 18 | from tailbone import DEBUG 19 | from tailbone import PREFIX 20 | 21 | from google.appengine.ext import blobstore 22 | from google.appengine.ext.webapp import blobstore_handlers 23 | from google.appengine.api import lib_config 24 | from google.appengine.api import app_identity 25 | 26 | class _ConfigDefaults(object): 27 | BUCKET = app_identity.get_application_id() 28 | LOCAL = 'client/data' 29 | 30 | _config = lib_config.register('tailboneCloudstore', _ConfigDefaults.__dict__) 31 | 32 | 33 | class ServeHandler(blobstore_handlers.BlobstoreDownloadHandler): 34 | def get(self, resource): 35 | if DEBUG: 36 | filename = "{}/{}".format(_config.LOCAL, resource) 37 | self.response.write(open(filename).read()) 38 | return 39 | filename = "/gs/{}/{}".format(_config.BUCKET, resource) 40 | key = blobstore.create_gs_key(filename) 41 | self.send_blob(key) 42 | 43 | app = webapp2.WSGIApplication([ 44 | (r"{}cloudstore/(.*)".format(PREFIX), ServeHandler) 45 | ], debug=DEBUG) 46 | -------------------------------------------------------------------------------- /tailbone/cloudstore/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/cloudstore/?.* 17 | script: tailbone.cloudstore.app 18 | -------------------------------------------------------------------------------- /tailbone/compute_engine/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 |
26 |

27 | Admin panel for monitoring the mesh network 28 |

29 |
38 | 39 | -------------------------------------------------------------------------------- /tailbone/compute_engine/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/compute_engine/?.* 17 | script: tailbone.compute_engine.app 18 | login: admin 19 | - url: /admin/compute_engine 20 | static_files: tailbone/compute_engine/admin.html 21 | upload: tailbone/compute_engine/admin.html 22 | login: admin 23 | 24 | admin_console: 25 | pages: 26 | - name: Load Balancer 27 | url: /admin/compute_engine -------------------------------------------------------------------------------- /tailbone/customce/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tailbone import BaseHandler 16 | from tailbone import as_json 17 | from tailbone import AppError 18 | from tailbone import config 19 | from tailbone import DEBUG 20 | from tailbone import PREFIX 21 | from tailbone.compute_engine import LoadBalancer 22 | from tailbone.compute_engine import TailboneCEInstance 23 | from tailbone.compute_engine import STARTUP_SCRIPT_BASE 24 | 25 | import binascii 26 | from hashlib import sha1 27 | import hmac 28 | import md5 29 | import time 30 | import webapp2 31 | 32 | from google.appengine.api import lib_config 33 | 34 | 35 | class _ConfigDefaults(object): 36 | PARAMS = {} 37 | SOURCE_SNAPSHOT = None 38 | STARTUP_SCRIPT = """ 39 | echo "You should edit the appengine_config.py file with your own startup_script." 40 | """ 41 | def calc_load(stats): 42 | return TailboneCEInstance.calc_load(stats) 43 | 44 | 45 | _config = lib_config.register('tailboneCustomCE', _ConfigDefaults.__dict__) 46 | 47 | # Prefixing internal models with Tailbone to avoid clobbering when using RESTful API 48 | class TailboneCustomInstance(TailboneCEInstance): 49 | SOURCE_SNAPSHOT = _config.SOURCE_SNAPSHOT 50 | PARAMS = dict(dict(TailboneCEInstance.PARAMS, **{ 51 | "name": "custom-id", 52 | "metadata": { 53 | "items": [ 54 | { 55 | "key": "startup-script", 56 | "value": STARTUP_SCRIPT_BASE + _config.STARTUP_SCRIPT, 57 | }, 58 | ], 59 | } 60 | }), **_config.PARAMS) 61 | 62 | @staticmethod 63 | def calc_load(stats): 64 | return _config.calc_load(stats) 65 | 66 | 67 | class CustomHandler(BaseHandler): 68 | @as_json 69 | def get(self): 70 | instance = LoadBalancer.find(TailboneCustomInstance) 71 | if not instance: 72 | raise AppError('Instance not found, try again later.') 73 | return { 74 | "ip": instance.address 75 | } 76 | 77 | app = webapp2.WSGIApplication([ 78 | (r"{}customce/?.*".format(PREFIX), CustomHandler), 79 | ], debug=DEBUG) -------------------------------------------------------------------------------- /tailbone/customce/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/customce/?.* 17 | script: tailbone.customce.app -------------------------------------------------------------------------------- /tailbone/dependencies.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/dependencies.zip -------------------------------------------------------------------------------- /tailbone/files/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # shared resources and global variables 16 | from tailbone import AppError 17 | from tailbone import PREFIX 18 | from tailbone import as_json 19 | from tailbone import BreakError 20 | from tailbone import config 21 | from tailbone import DEBUG 22 | from tailbone import restful 23 | 24 | import re 25 | import urllib 26 | import webapp2 27 | 28 | HAS_PIL = True 29 | try: 30 | import PIL 31 | from google.appengine.api.images import delete_serving_url 32 | from google.appengine.api.images import get_serving_url_async 33 | except: 34 | HAS_PIL = False 35 | 36 | from google.appengine.ext.ndb import blobstore 37 | from google.appengine.ext import blobstore as bs 38 | from google.appengine.ext.webapp import blobstore_handlers 39 | from google.appengine.api import lib_config 40 | 41 | class _ConfigDefaults(object): 42 | CLOUDSTORE = None 43 | 44 | _config = lib_config.register('tailboneFiles', _ConfigDefaults.__dict__) 45 | 46 | 47 | re_image = re.compile(r"image/(png|jpeg|jpg|webp|gif|bmp|tiff|ico)", re.IGNORECASE) 48 | 49 | 50 | class BlobInfo(blobstore.BlobInfo): 51 | def to_dict(self, *args, **kwargs): 52 | result = super(BlobInfo, self).to_dict(*args, **kwargs) 53 | result["Id"] = str(self.key()) 54 | return result 55 | 56 | 57 | def blob_info_to_dict(blob_info): 58 | d = {} 59 | for prop in ["content_type", "creation", "filename", "size"]: 60 | d[prop] = getattr(blob_info, prop) 61 | key = blob_info.key() 62 | if HAS_PIL and re_image.match(blob_info.content_type): 63 | d["image_url"] = get_serving_url_async(key) 64 | d["Id"] = str(key) 65 | return d 66 | 67 | 68 | class FilesHandler(blobstore_handlers.BlobstoreDownloadHandler): 69 | 70 | @as_json 71 | def get(self, key): 72 | if key == "": # query 73 | if not config.is_current_user_admin(): 74 | raise AppError("User must be administrator.") 75 | return restful.query(self, BlobInfo) 76 | elif key == "create": 77 | #, gs_bucket_name=_config.CLOUDSTORE) 78 | return { 79 | "upload_url": blobstore.create_upload_url("/api/files/upload") 80 | } 81 | key = str(urllib.unquote(key)) 82 | blob_info = bs.BlobInfo.get(key) 83 | if blob_info: 84 | self.send_blob(blob_info) 85 | raise BreakError 86 | else: 87 | self.error(404) 88 | return {"error": "File not found with key " + key} 89 | 90 | @as_json 91 | def post(self, _): 92 | raise AppError("You must make a GET call to /api/files/create to get a POST url.") 93 | 94 | @as_json 95 | def put(self, _): 96 | raise AppError("PUT is not supported for the files api.") 97 | 98 | @as_json 99 | def delete(self, key): 100 | if not config.is_current_user_admin(): 101 | raise AppError("User must be administrator.") 102 | key = blobstore.BlobKey(str(urllib.unquote(key))) 103 | blob_info = BlobInfo.get(key) 104 | if blob_info: 105 | blob_info.delete() 106 | if HAS_PIL and re_image.match(blob_info.content_type): 107 | delete_serving_url(key) 108 | return {} 109 | else: 110 | self.error(404) 111 | return {"error": "File not found with key " + key} 112 | 113 | 114 | class FilesUploadHandler(blobstore_handlers.BlobstoreUploadHandler): 115 | @as_json 116 | def post(self): 117 | return [blob_info_to_dict(b) for b in self.get_uploads()] 118 | 119 | 120 | app = webapp2.WSGIApplication([ 121 | (r"{}files/upload".format(PREFIX), FilesUploadHandler), 122 | (r"{}files/?(.*)".format(PREFIX), FilesHandler), 123 | ], debug=DEBUG) 124 | -------------------------------------------------------------------------------- /tailbone/files/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/files/?.* 17 | script: tailbone.files.app 18 | -------------------------------------------------------------------------------- /tailbone/geoip/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tailbone import as_json 16 | from tailbone import BaseHandler 17 | from tailbone import DEBUG 18 | 19 | import webapp2 20 | import json 21 | 22 | class GeoIPHandler(BaseHandler): 23 | @as_json 24 | def get(self): 25 | if DEBUG: 26 | return { 27 | "Country": "US", 28 | "Region": "ca", 29 | "CityLatLong": { 30 | "lat": 37.7749, 31 | "lon": -122.4194, 32 | }, 33 | "IP": "127.0.0.1", 34 | "City": "san francisco", 35 | } 36 | resp = {} 37 | for x in ["Country", "Region", "City", "CityLatLong"]: 38 | k = "X-AppEngine-" + x 39 | value = self.request.headers.get(k) 40 | if x == "CityLatLong" and value: 41 | value = [float(v) for v in value.split(",")] 42 | value = { 43 | "lat": value[0], 44 | "lon": value[1], 45 | } 46 | resp[x] = value 47 | resp["IP"] = self.request.remote_addr 48 | return resp 49 | 50 | app = webapp2.WSGIApplication([ 51 | (r".*", GeoIPHandler), 52 | ], debug=DEBUG) 53 | -------------------------------------------------------------------------------- /tailbone/geoip/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/geoip 17 | script: tailbone.geoip.app -------------------------------------------------------------------------------- /tailbone/globals.js: -------------------------------------------------------------------------------- 1 | // globals.js 2 | // TODO: need a better way to handle this with proper dependency managment with multiple imports 3 | 4 | (function() { 5 | 6 | // 2014-02-28T04:29:28.222000 7 | var reISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)$/; 8 | 9 | JSON._parse = JSON.parse; 10 | JSON.parse = function(json) {return JSON._parse(json, function(key, value) { 11 | if (typeof value === 'string') { 12 | if (reISO.exec(value)) { 13 | return new Date(value); 14 | } 15 | } 16 | return value; 17 | }); 18 | }; 19 | if (window.jQuery !== undefined) { 20 | jQuery.parseJSON = JSON.parse; 21 | jQuery.ajaxSettings.converters["text json"] = JSON.parse; 22 | } 23 | 24 | })(); 25 | 26 | var http = {}; 27 | (function() { 28 | 29 | function json_request(kind, url, success, error, context) { 30 | var xhr; 31 | if (XMLHttpRequest) { 32 | xhr = new XMLHttpRequest(); 33 | } else { 34 | xhr = new ActiveXObject("Microsoft.XMLHTTP"); 35 | } 36 | xhr.open(kind, url, true); 37 | xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); 38 | xhr.setRequestHeader("If-None-Match", "some-random-string"); 39 | xhr.setRequestHeader("Cache-Control", "no-cache,max-age=0"); 40 | xhr.setRequestHeader("Pragma", "no-cache"); 41 | 42 | xhr.onreadystatechange = function() { 43 | if (xhr.readyState === 4) { 44 | var data = xhr.responseText; 45 | try { data = JSON.parse(data); } catch(e) {} 46 | var args = Array.prototype.slice.call(arguments); 47 | args.unshift(data); 48 | args.push(xhr); 49 | if (xhr.status === 200 && typeof success === 'function') { 50 | success.apply(context || xhr, args); 51 | } else if (typeof error === 'function') { 52 | error.apply(context || xhr, args); 53 | } else { 54 | if (console) { 55 | console.warn('Made Ajax request but set no callback.'); 56 | } 57 | } 58 | } 59 | }; 60 | return xhr; 61 | } 62 | http.HEAD = function(url, load, error, context) { 63 | xhr = json_request('HEAD', url, load, error, context); 64 | xhr.send(); 65 | }; 66 | http.GET = function(url, load, error, context) { 67 | xhr = json_request('GET', url, load, error, context); 68 | xhr.send(); 69 | }; 70 | http.POST = function(url, data, load, error, context) { 71 | xhr = json_request('POST', url, load, error, context); 72 | xhr.send(JSON.stringify(data)); 73 | }; 74 | http.DELETE = function(url, load, error, context) { 75 | xhr = json_request('DELETE', url, load, error, context); 76 | xhr.send(); 77 | }; 78 | 79 | })(); 80 | -------------------------------------------------------------------------------- /tailbone/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /tailbone.js 17 | script: tailbone.app 18 | - url: /api/log(in|out|up)/? 19 | script: tailbone.auth 20 | -------------------------------------------------------------------------------- /tailbone/mesh/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # shared resources and global variables 16 | from tailbone import as_json, BaseHandler, compile_js, AppError 17 | from tailbone import config 18 | from tailbone import DEBUG 19 | from tailbone import PREFIX 20 | from tailbone import turn 21 | from tailbone.compute_engine import LoadBalancer 22 | from tailbone.compute_engine import TailboneCEInstance 23 | from tailbone.compute_engine import STARTUP_SCRIPT_BASE 24 | 25 | import base64 26 | import json 27 | import os 28 | import random 29 | import string 30 | import webapp2 31 | 32 | from google.appengine.api import users 33 | from google.appengine.api import memcache 34 | from google.appengine.api import app_identity 35 | from google.appengine.api import lib_config 36 | 37 | # TODO: Use an image instead of a startup-script for downloading dependencies 38 | 39 | # Prefixing internal models with Tailbone to avoid clobbering when using RESTful API 40 | 41 | class _ConfigDefaults(object): 42 | ROOM_EXPIRATION = 86400 # one day in seconds 43 | DEBUG_WEBSOCKET_ADDRESS = None 44 | ENABLE_WEBSOCKET = False 45 | ENABLE_TURN = False 46 | PORT = 8889 47 | SOURCE_SNAPSHOT = None 48 | PARAMS = {} 49 | 50 | def generate_room_name(): 51 | return generate_word() + "." + generate_word() 52 | 53 | 54 | _config = lib_config.register('tailboneMesh', _ConfigDefaults.__dict__) 55 | 56 | class TailboneWebsocketInstance(TailboneCEInstance): 57 | SOURCE_SNAPSHOT = _config.SOURCE_SNAPSHOT 58 | PARAMS = dict(dict(TailboneCEInstance.PARAMS, **{ 59 | "name": "websocket-id", 60 | "metadata": { 61 | "items": [ 62 | { 63 | "key": "startup-script", 64 | "value": STARTUP_SCRIPT_BASE + """ 65 | # websocket server 66 | ./nodejs/bin/npm install ws 67 | cat >websocket.js < 0) { 103 | NodeUtils.sendWrapper = function() { 104 | var ctx = this; 105 | var args = arguments; 106 | setTimeout(function() { 107 | NodeUtils.send.apply(ctx, args); 108 | }, delay); 109 | } 110 | } 111 | } else if (typeof delay === "function") { 112 | NodeUtils.sendWrapper = function() { 113 | var ctx = this; 114 | var args = arguments; 115 | setTimeout(function() { 116 | NodeUtils.send.apply(ctx, args); 117 | }, delay()) 118 | } 119 | } 120 | 121 | }; 122 | 123 | /** 124 | * Connects self Node to the mesh. 125 | * If WebSocket IP is specified, attempts direct connection. 126 | * Otherwise, attempts to retrieve config object from load balancer via its API url. 127 | */ 128 | Mesh.prototype.connect = function () { 129 | 130 | var self = this, 131 | options, 132 | idMatch; 133 | 134 | if (this.options.ws) { 135 | 136 | idMatch = this.options.ws.match('[^\/]+$'); 137 | if (idMatch) { 138 | this.id = idMatch[0]; 139 | } 140 | 141 | this.self.connect(); 142 | this.peers.forEach(function (peer) { 143 | peer.connect(); 144 | }); 145 | 146 | } else if (this.options.channel) { 147 | 148 | this.id = this.options.name; 149 | 150 | this.self.connect(); 151 | this.peers.forEach(function (peer) { 152 | peer.connect(); 153 | }); 154 | 155 | } else if (this.options.api) { 156 | 157 | http.GET(this.options.api + '/' + (this.id || ''), function (options) { 158 | 159 | self.config(options); 160 | self.connect(); 161 | 162 | }, function () { 163 | 164 | console.warn("Error connecting to server, retrying in 10 seconds."); 165 | setTimeout(function() { 166 | self.connect(); 167 | }, 10*1000); 168 | 169 | // throw new Error('Could not establish connection with server'); 170 | 171 | }); 172 | 173 | } else { 174 | 175 | throw new Error('Invalid options'); 176 | 177 | } 178 | 179 | }; 180 | 181 | /** 182 | * Disconnects all Nodes 183 | */ 184 | Mesh.prototype.disconnect = function () { 185 | 186 | this.self.disconnect(); 187 | 188 | this.peers.forEach(function (peer) { 189 | peer.disconnect(); 190 | }); 191 | 192 | }; 193 | 194 | /** 195 | * Binds event 196 | * @param type 197 | * @param handler 198 | */ 199 | Mesh.prototype.bind = function (type, handler) { 200 | 201 | EventDispatcher.prototype.bind.apply(this, arguments); 202 | this.peers._bind.apply(this.peers, arguments); 203 | 204 | }; 205 | 206 | /** 207 | * Unbinds event 208 | * @param type 209 | * @param handler 210 | */ 211 | Mesh.prototype.unbind = function (type, handler) { 212 | 213 | EventDispatcher.prototype.unbind.apply(this, arguments); 214 | this.peers._unbind.apply(this.peers, arguments); 215 | 216 | }; 217 | 218 | /** 219 | * Triggers event 220 | * @param type 221 | * @param args 222 | */ 223 | Mesh.prototype.trigger = function (type, args) { 224 | 225 | this.self.trigger.apply(this.self, arguments); 226 | this.peers.trigger.apply(this.peers, arguments); 227 | 228 | }; 229 | 230 | 231 | /** 232 | * Common Mesh options 233 | * @type {{api: '/api/mesh', autoConnect: boolean}} 234 | */ 235 | Mesh.options = { 236 | 237 | api: '/api/mesh', 238 | autoConnect: true, 239 | autoPeerConnect: true, 240 | useWebRTC: true, 241 | delay: undefined 242 | 243 | }; 244 | -------------------------------------------------------------------------------- /tailbone/mesh/js/NetChannel.js: -------------------------------------------------------------------------------- 1 | // https://github.com/publicclass/netchan 2 | 3 | /** 4 | * NetChannel wraps an unreliable DataChannel 5 | * with a sequence and an ack. 6 | * 7 | * When a message is received it checks the ack against 8 | * the messages buffer and the ones "acknowledged" will 9 | * be removed from the buffer and the rest will be resent. 10 | * 11 | * After sending and the buffer is not empty after a timeout 12 | * it will try to send again until it is. 13 | * 14 | * Inspired by NetChan by Id software. 15 | * 16 | * Options: 17 | * 18 | * - {Number} `resend` a number in ms if how often it should try to flush again. 19 | * - {Boolean} `ack` if true an ACK packet will automatically be responded with to keep the buffer clean. 20 | * 21 | * @param {DataChannel} channel 22 | * @param {Object} opts 23 | */ 24 | function NetChannel(channel,opts){ 25 | this.seq = 1; 26 | this.ack = 0; 27 | this.buffer = []; // [seq,buf] 28 | this.bufferLength = 0; 29 | this.encoded = null; // cached 30 | this.options = opts || {resend: 500}; 31 | 32 | channel && this.setChannel(channel) 33 | } 34 | 35 | // magic packet 36 | var ACK = 'ncACK'.split('').map(function(c){return c.charCodeAt(0)}) 37 | NetChannel.ACK = new Uint8Array(ACK).buffer; 38 | NetChannel._isACK = function (msg) { 39 | // check the type 40 | if( !msg || typeof msg != typeof NetChannel.ACK ){ 41 | return false; 42 | } 43 | 44 | // check the length 45 | if( msg.byteLength !== NetChannel.ACK.byteLength ){ 46 | return false; 47 | } 48 | // check if they have the same contents 49 | var arr = new Uint8Array(msg); 50 | for(var i=0; i 255 ){ 88 | throw new Error('invalid message length, only up to 256 bytes are supported') 89 | } 90 | 91 | // grow by 3 bytes (seq & len) 92 | var seq = this.seq++; 93 | var buf = new Uint8Array(3+msg.byteLength); 94 | var dat = new DataView(buf.buffer); 95 | dat.setUint16(0,seq); 96 | dat.setUint8(2,msg.byteLength); 97 | buf.set(new Uint8Array(msg),3); 98 | 99 | this.bufferLength += buf.byteLength; 100 | this.buffer.push(seq,buf); 101 | this.encoded = null; 102 | 103 | this.flush(); 104 | }, 105 | 106 | flush: function(){ 107 | if( this.bufferLength && this.channel && this.channel.readyState == 'open' ){ 108 | this.channel.send(this.encoded || this.encode()); 109 | 110 | // try again every X ms and stop when buffer is empty 111 | if( this.options.resend ){ 112 | clearTimeout(this._timeout) 113 | this._timeout = setTimeout(this.flush.bind(this),this.options.resend) 114 | } 115 | } 116 | }, 117 | 118 | // encodes into a message like this: 119 | // ack,seq1,len1,data1[,seq2,len2,data2...] 120 | encode: function(){ 121 | // grow by 2 bytes (ack) + unsent buffer 122 | var buf = new Uint8Array(2+this.bufferLength); 123 | var data = new DataView(buf.buffer); 124 | 125 | // prepend with ack number 126 | data.setUint16(0,this.ack) 127 | 128 | // write all buffered messages 129 | var offset = 2; 130 | for(var i=1; i < this.buffer.length; i+=2){ 131 | var msg = this.buffer[i]; 132 | buf.set(msg,offset); 133 | offset += msg.byteLength; 134 | } 135 | return this.encoded = buf.buffer; 136 | }, 137 | 138 | // decodes from a message like this: 139 | // ack,seq1,len1,data1[,seq2,len2,data2...] 140 | decode: function(buf){ 141 | // read the sequence and ack 142 | var data = new DataView(buf.buffer || buf) 143 | var ack = data.getUint16(0) 144 | this.shrink(ack) 145 | 146 | // read messages 147 | var offset = 2 // start after ack 148 | , length = buf.byteLength 149 | , seq = this.ack // in case no messages are read, its the same 150 | , len = 0 151 | , sendACK = false; 152 | 153 | while(offset < length){ 154 | seq = data.getUint16(offset,false); // false is required for node test only 155 | len = data.getUint8(offset+2); 156 | if( seq <= this.ack ){ 157 | offset += len+3; // len + seq = 3 bytes 158 | continue; 159 | } 160 | 161 | // get the message 162 | var msg = data.buffer.slice(offset+3,offset+3+len); 163 | offset += len+3; 164 | 165 | // emit onmessage for each message unless it's an ACK 166 | if( !this.options.ack || !NetChannel._isACK(msg) ){ 167 | if( typeof this.onmessage == 'function' ){ 168 | this.onmessage(msg); 169 | } 170 | sendACK = true; 171 | } 172 | 173 | // store the sequence as the last acknowledged one 174 | this.ack = seq; 175 | } 176 | 177 | // send an ACK 178 | if( this.options.ack && sendACK ){ 179 | this.send(NetChannel.ACK); 180 | } 181 | }, 182 | 183 | // shrink the buffer & bufferLength up to the 184 | // acknowledged messages. 185 | // assumes this.buffer is sorted by sequence 186 | shrink: function(ack){ 187 | var index = null 188 | , length = 0; 189 | for(var i=0; i < this.buffer.length; i+=2){ 190 | var s = this.buffer[i]; 191 | if( s <= ack ){ 192 | index = i+2; 193 | length += this.buffer[i+1].byteLength; 194 | } else { 195 | break; 196 | } 197 | } 198 | if( index !== null ){ 199 | this.buffer.splice(0,index); 200 | this.bufferLength -= length; 201 | this.encoded = null; 202 | } 203 | }, 204 | 205 | toString: function(){ 206 | return 'NetChannel\n\t' + [ 207 | 'seq: '+this.seq, 208 | 'ack: '+this.ack, 209 | 'buffer: '+this.buffer.length, 210 | 'buffer size: '+this.bufferLength, 211 | 'encoded: '+(this.encoded&&this.encoded.byteLength) 212 | ].join('\n\t') 213 | } 214 | } 215 | 216 | -------------------------------------------------------------------------------- /tailbone/mesh/js/Node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Doug Fritz dougfritz@google.com 3 | * @author Maciej Zasada maciej@unit9.com 4 | * Date: 6/2/13 5 | * Time: 11:28 PM 6 | */ 7 | 8 | /** 9 | * Internal Node utility functions 10 | * @type {{PROTECTED_EVENTS: Array, uidSeed: number, remoteBindsByNodeIds: {}, upgradeLocal: Function, upgradeRemote: Function, upgradeMutual: Function, send: Function, acknowledgeRemoteBind: Function, acknowledgeRemoteUnbind: Function, doesRemoteBindTo: Function}} 11 | */ 12 | var NodeUtils = { 13 | 14 | PROTECTED_EVENTS: ['open', 'close', 'error', 'connect', 'enter', 'leave', 'bind', 'unbind'], 15 | 16 | uidSeed: 1, 17 | remoteBindsByNodeIds: {}, 18 | 19 | send: function (node, message) { 20 | 21 | var sendChannel = node._channels.filter(function(c) { 22 | return c.getState() === Channel.STATE.OPEN; 23 | })[0]; 24 | 25 | if (sendChannel) { 26 | sendChannel.send(message); 27 | } else { 28 | console.warn('There is no open send channel.'); 29 | } 30 | 31 | }, 32 | 33 | acknowledgeRemoteBind: function (nodeId, type) { 34 | 35 | NodeUtils.remoteBindsByNodeIds[nodeId] = NodeUtils.remoteBindsByNodeIds[nodeId] || []; 36 | if (NodeUtils.remoteBindsByNodeIds[nodeId].indexOf(type) === -1) { 37 | NodeUtils.remoteBindsByNodeIds[nodeId].push(type); 38 | } 39 | 40 | }, 41 | 42 | acknowledgeRemoteUnbind: function (nodeId, type) { 43 | 44 | var index; 45 | if (NodeUtils.remoteBindsByNodeIds[nodeId] && (index = NodeUtils.remoteBindsByNodeIds[nodeId].indexOf(type))) { 46 | NodeUtils.remoteBindsByNodeIds[nodeId].splice(index, 1); 47 | } 48 | 49 | }, 50 | 51 | doesRemoteBindTo: function (nodeId, type) { 52 | 53 | // if nothing has been external bound assume all things are 54 | var types = NodeUtils.remoteBindsByNodeIds[nodeId]; 55 | if (types === undefined) { 56 | return true; 57 | } 58 | 59 | return types && NodeUtils.remoteBindsByNodeIds[nodeId].indexOf(type) !== -1; 60 | 61 | } 62 | 63 | }; 64 | 65 | // Proxy send function so it can be overridden with out a performance hit 66 | NodeUtils.sendWrapper = NodeUtils.send; 67 | 68 | /** 69 | * Node 70 | * @param mesh {Mesh} Mesh to which the node belongs 71 | * @param id {string} Node ID 72 | * @constructor 73 | */ 74 | var Node = function (mesh, id, initiator) { 75 | 76 | StateDrive.call(this); 77 | 78 | var uid = NodeUtils.uidSeed++; 79 | this.__defineGetter__('uid', function () { 80 | return uid; 81 | }); 82 | this.mesh = mesh; 83 | this.id = id; 84 | this._channels = []; 85 | this._signalingChannel = null; 86 | this._remotelyBoundTypes = {}; 87 | 88 | this.__defineGetter__('initiator', function () { 89 | 90 | return initiator; 91 | 92 | }); 93 | 94 | this.setState(Node.STATE.DISCONNECTED); 95 | this.setMinCallState("connect", Node.STATE.DISCONNECTED); 96 | this.setMinCallState("disconnect", Node.STATE.CONNECTED); 97 | this.setMinCallState("bind", Node.STATE.CONNECTED); 98 | this.setMinCallState("unbind", Node.STATE.CONNECTED); 99 | this.setMinCallState("_bind", Node.STATE.CONNECTED); 100 | this.setMinCallState("_unbind", Node.STATE.CONNECTED); 101 | this.setMinCallState("trigger", Node.STATE.CONNECTED); 102 | 103 | }; 104 | 105 | /** 106 | * Extend StateDrive 107 | * @type {StateDrive} 108 | */ 109 | Node.prototype = new StateDrive(); 110 | 111 | /** 112 | * Returns unique Node string representation. 113 | * Essential to make dictionary indexing by Node work. 114 | * @returns {string} 115 | */ 116 | Node.prototype.toString = function () { 117 | 118 | return 'Node@' + this.uid; 119 | 120 | }; 121 | 122 | /** 123 | * Connects to remote node 124 | */ 125 | Node.prototype.connect = function (callback) { 126 | 127 | var self = this; 128 | var state = this.getState(); 129 | if (state === Node.STATE.CONNECTING || state === Node.STATE.CONNECTED) { 130 | return; 131 | } 132 | 133 | self.setState(Node.STATE.CONNECTING); 134 | 135 | if (this.mesh.options.ws) { 136 | this._signalingChannel = new SocketChannel(this.mesh.self, this); 137 | } else { 138 | this._signalingChannel = new ChannelChannel(this.mesh.self, this); 139 | } 140 | 141 | if (self !== self.mesh.self && self.mesh.options.useWebRTC) { 142 | this._channels.push(new RTCChannel(this.mesh.self, this)); 143 | } 144 | this._channels.push(this._signalingChannel); 145 | 146 | var propagateMessage = function(e) { 147 | var args = self.preprocessIncoming(e.data); 148 | // propagate up 149 | EventDispatcher.prototype.trigger.apply(self, args); 150 | if (self !== self.mesh.self) { 151 | EventDispatcher.prototype.trigger.apply(self.mesh.peers, args); 152 | } 153 | EventDispatcher.prototype.trigger.apply(self.mesh, args); 154 | }; 155 | 156 | this._channels.forEach(function (channel) { 157 | ['open', 'message', 'error', 'close'].forEach(function(type) { 158 | channel.bind(type, propagateMessage); 159 | }); 160 | }); 161 | 162 | this._signalingChannel.bind('open', function(e) { 163 | //Broadcast to all newly bound nodes all of your current listeners 164 | if (self != self.mesh.self) { 165 | var types = Object.keys(self.mesh._handlers); 166 | var peers = Object.keys(self.mesh.peers._handlers); 167 | peers.forEach(function(type) { 168 | if (types.indexOf(type) === -1) { 169 | types.push(type); 170 | } 171 | }); 172 | types.forEach(function(type) { 173 | self._bind(type); 174 | }); 175 | // open all other channels upgrading to webrtc where possible 176 | self._channels.forEach(function(channel) { 177 | channel.open(); 178 | }); 179 | } 180 | self.setState(Node.STATE.CONNECTED); 181 | }); 182 | 183 | this._signalingChannel.open(); 184 | 185 | }; 186 | 187 | /** 188 | * Disconnects node 189 | */ 190 | Node.prototype.disconnect = function () { 191 | 192 | this._channels.forEach(function (channel) { 193 | 194 | channel.unbind('message'); 195 | channel.close(); 196 | 197 | }); 198 | 199 | }; 200 | 201 | /** 202 | * Binds an event on the remote node 203 | * @param type {string} 204 | * @param handler {function} 205 | */ 206 | Node.prototype.bind = function (type, handler) { 207 | EventDispatcher.prototype.bind.apply(this, arguments); 208 | this._bind.apply(this, arguments); 209 | }; 210 | 211 | Node.prototype._bind = function(type, handler) { 212 | if (this !== this.mesh.self) { 213 | if (NodeUtils.PROTECTED_EVENTS.indexOf(type) === -1) { 214 | var bound = this._remotelyBoundTypes[type]; 215 | if (!bound) { 216 | this._remotelyBoundTypes[type] = 0; 217 | NodeUtils.sendWrapper(this, '["bind","' + type + '"]'); 218 | } 219 | this._remotelyBoundTypes[type] += 1; 220 | } 221 | } 222 | }; 223 | 224 | /** 225 | * Unbinds event from the remote node 226 | * @param type {string} 227 | * @param handler {function} 228 | */ 229 | Node.prototype.unbind = function (type, handler) { 230 | EventDispatcher.prototype.unbind.apply(this, arguments); 231 | this._unbind.apply(this, arguments); 232 | }; 233 | 234 | Node.prototype._unbind = function(type, handler) { 235 | if (this !== this.mesh.self) { 236 | if (NodeUtils.PROTECTED_EVENTS.indexOf(type) === -1) { 237 | this._remotelyBoundTypes[type] -= 1; 238 | var bound = this._remotelyBoundTypes[type]; 239 | if (bound === 0) { 240 | NodeUtils.sendWrapper(this, '["unbind","' + type + '"]'); 241 | } 242 | } 243 | } 244 | }; 245 | 246 | /** 247 | * Triggers remotely on the node 248 | * @param type 249 | * @param args 250 | */ 251 | Node.prototype.trigger = function (type, args) { 252 | 253 | // Trigger on self 254 | if (this === this.mesh.self) { 255 | EventDispatcher.prototype.trigger.apply(this, arguments); 256 | // propagate up 257 | EventDispatcher.prototype.trigger.apply(this.mesh, arguments); 258 | return; 259 | } 260 | 261 | if (!NodeUtils.doesRemoteBindTo(this.id, type)) { 262 | return; 263 | } 264 | 265 | this._trigger.apply(this, arguments); 266 | 267 | }; 268 | 269 | /** 270 | * Sends to remote regardless of if it asked for it. 271 | * Useful for upgrading rtc connections before things are bound fully. 272 | */ 273 | Node.prototype._trigger = function (type, args) { 274 | 275 | var message; 276 | 277 | try { 278 | var outgoing = this.preprocessOutgoing.apply(this, arguments); 279 | message = JSON.stringify(Array.prototype.slice.apply(outgoing)); 280 | if (message === 'null') { 281 | return; 282 | } 283 | 284 | } catch (e) { 285 | 286 | throw new Error('Trigger not serializable'); 287 | 288 | } 289 | 290 | NodeUtils.sendWrapper(this, message); 291 | 292 | }; 293 | 294 | /** 295 | * Pre-processes incoming event before passing it on to the event pipeline 296 | * @param eventArguments {array} data 297 | */ 298 | Node.prototype.preprocessIncoming = function (eventArguments) { 299 | 300 | var type = eventArguments[0], 301 | parsedArguments = [], 302 | node, 303 | i; 304 | 305 | switch (type) { 306 | 307 | case 'connect': 308 | parsedArguments.push(type); 309 | for (i = 1; i < eventArguments.length; ++i) { 310 | node = new Node(this.mesh, eventArguments[i], true); 311 | parsedArguments.push(node); 312 | // add node 313 | this.mesh.peers.push(node); 314 | if (this.mesh.options.autoPeerConnect) { 315 | node.connect(); 316 | } 317 | } 318 | if (this != this.mesh.self) { 319 | console.warn('Expected "connect" triggered only on self node.') 320 | } 321 | // mark mesh and peers as connected 322 | this.mesh.peers.setState(Node.STATE.CONNECTED); 323 | this.mesh.setState(Node.STATE.CONNECTED); 324 | break; 325 | 326 | case 'enter': 327 | parsedArguments.push(type); 328 | for (i = 1; i < eventArguments.length; ++i) { 329 | node = new Node(this.mesh, eventArguments[i], false); 330 | parsedArguments.push(node); 331 | // add node 332 | this.mesh.peers.push(node); 333 | if (this.mesh.options.autoPeerConnect) { 334 | node.connect(); 335 | } 336 | } 337 | break; 338 | 339 | case 'leave': 340 | parsedArguments.push(type); 341 | // TODO: these node objects are not equal to those in the peers list 342 | for (i = 1; i < eventArguments.length; ++i) { 343 | node = this.mesh.peers.getById(eventArguments[i]); 344 | if (node) { 345 | parsedArguments.push(node); 346 | // remove node 347 | this.mesh.peers.splice(this.mesh.peers.indexOf(node), 1); 348 | node.disconnect(); 349 | } else { 350 | console.warn('Node', eventArguments[i], 'leave event but not in peers.'); 351 | } 352 | } 353 | break; 354 | 355 | case 'bind': 356 | NodeUtils.acknowledgeRemoteBind(this.id, eventArguments[1]); 357 | parsedArguments = eventArguments; 358 | break; 359 | 360 | case 'unbind': 361 | NodeUtils.acknowledgeRemoteUnbind(this.id, eventArguments[1]); 362 | parsedArguments = eventArguments; 363 | break; 364 | 365 | default: 366 | parsedArguments = eventArguments; 367 | break; 368 | 369 | } 370 | 371 | return parsedArguments; 372 | 373 | }; 374 | 375 | /** 376 | * Pre-processes outgoing events before sending them 377 | * @param type {string} event type 378 | * @param args {object...} event arguments 379 | * @returns {Arguments} processed message array ready to be sent 380 | */ 381 | Node.prototype.preprocessOutgoing = function (type, args) { 382 | 383 | if (NodeUtils.PROTECTED_EVENTS.indexOf(type) === -1) { 384 | return arguments; 385 | } else { 386 | throw new Error('Event type ' + type + ' protected'); 387 | } 388 | 389 | }; 390 | 391 | Node.STATE = { 392 | 393 | DISCONNECTED: 1, 394 | CONNECTING: 2, 395 | CONNECTED: 3 396 | 397 | } -------------------------------------------------------------------------------- /tailbone/mesh/js/Peers.js: -------------------------------------------------------------------------------- 1 | // TODO: should forEach and filter and such be moved to more compatible syntax? 2 | 3 | var Peers = function() { 4 | Array.call(this); 5 | StateDrive.call(this); 6 | 7 | this.setState(Node.STATE.DISCONNECTED); 8 | 9 | this.setMinCallState('bind', Node.STATE.CONNECTED); 10 | this.setMinCallState('unbind', Node.STATE.CONNECTED); 11 | this.setMinCallState('trigger', Node.STATE.CONNECTED); 12 | }; 13 | 14 | Peers.prototype = []; 15 | for (var k in StateDrive.prototype) { 16 | Peers.prototype[k] = StateDrive.prototype[k]; 17 | } 18 | 19 | Peers.prototype.bind = function() { 20 | EventDispatcher.prototype.bind.apply(this, arguments); 21 | this._bind.apply(this, arguments); 22 | }; 23 | 24 | Peers.prototype._bind = function() { 25 | var originalArguments = arguments; 26 | this.forEach(function (peer) { 27 | peer._bind.apply(peer, originalArguments); 28 | }); 29 | }; 30 | 31 | Peers.prototype.unbind = function() { 32 | EventDispatcher.prototype.unbind.apply(this, arguments); 33 | this._unbind.apply(this, arguments); 34 | }; 35 | 36 | Peers.prototype._unbind = function() { 37 | var originalArguments = arguments; 38 | this.forEach(function (peer) { 39 | peer._unbind.apply(peer, originalArguments); 40 | }); 41 | }; 42 | 43 | Peers.prototype.trigger = function() { 44 | var originalArguments = arguments; 45 | this.forEach(function(peer) { 46 | peer.trigger.apply(peer, originalArguments); 47 | }); 48 | }; 49 | 50 | Peers.prototype.getById = function(node_id) { 51 | return this.filter(function(peer) { return peer.id === node_id; })[0]; 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /tailbone/mesh/js/SocketChannel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Doug Fritz dougfritz@google.com 3 | * @author Maciej Zasada maciej@unit9.com 4 | * Date: 6/4/13 5 | * Time: 3:01 AM 6 | */ 7 | 8 | /** 9 | * SocketChannel 10 | * @param localNode {Node} 11 | * @param remoteNode {Node} 12 | * @constructor 13 | */ 14 | var SocketChannel = function (localNode, remoteNode) { 15 | Channel.call(this, localNode, remoteNode); 16 | 17 | this.setState(Channel.STATE.CLOSED); 18 | this.setMinCallState('send', Channel.STATE.OPEN); 19 | this.setMinCallState('close', Channel.STATE.OPEN); 20 | 21 | this.multiplexer = SocketMultiplexer.get(localNode.mesh); 22 | }; 23 | 24 | /** 25 | * Extend Channel 26 | * @type {Channel} 27 | */ 28 | SocketChannel.prototype = new Channel(); 29 | 30 | /** 31 | * Opens WebSocket connection 32 | */ 33 | SocketChannel.prototype.open = function () { 34 | this.multiplexer.register(this); 35 | this.multiplexer.open(); 36 | }; 37 | 38 | /** 39 | * Closes WebSocket connection 40 | */ 41 | SocketChannel.prototype.close = function () { 42 | this.multiplexer.close(this); 43 | }; 44 | 45 | /** 46 | * Sends message to remoteNode 47 | * @param message {string} 48 | */ 49 | SocketChannel.prototype.send = function (message) { 50 | this.multiplexer.send(this, message); 51 | }; 52 | 53 | -------------------------------------------------------------------------------- /tailbone/mesh/js/SocketMultiplexer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Doug Fritz dougfritz@google.com 3 | * @author Maciej Zasada maciej@unit9.com 4 | * Date: 6/4/13 5 | * Time: 3:01 AM 6 | */ 7 | 8 | /** 9 | * Makes the attached channels behave like direct connections. 10 | */ 11 | var SocketMultiplexer = function(mesh) { 12 | StateDrive.call(this); 13 | this.mesh = mesh; 14 | this.setState(Channel.STATE.CLOSED); 15 | this.setMinCallState('send', Channel.STATE.OPEN); 16 | this.channels = {}; 17 | }; 18 | 19 | SocketMultiplexer._byMesh = {}; 20 | 21 | SocketMultiplexer.get = function(mesh) { 22 | var multiplexer = SocketMultiplexer._byMesh[mesh]; 23 | if(!multiplexer) { 24 | multiplexer = new SocketMultiplexer(mesh); 25 | SocketMultiplexer._byMesh[mesh] = multiplexer; 26 | } 27 | return multiplexer; 28 | }; 29 | 30 | SocketMultiplexer.prototype = new StateDrive(); 31 | 32 | SocketMultiplexer.prototype.register = function(channel) { 33 | if (this.channels[channel.remoteNode.id] === undefined) { 34 | this.channels[channel.remoteNode.id] = channel; 35 | var state = this.getState(); 36 | channel.setState(state); 37 | // if underlying websocket is already open simulate the open for the channel 38 | if (state === Channel.STATE.OPEN) { 39 | channel.trigger('open', { 40 | data: ['open'] 41 | }); 42 | } 43 | } 44 | }; 45 | 46 | SocketMultiplexer.prototype.unregister = function(channel) { 47 | delete this.channels[channel.remoteNode.id]; 48 | channel.setState(Channel.STATE.CLOSED); 49 | }; 50 | 51 | SocketMultiplexer.prototype.open = function(channel) { 52 | 53 | if (this.getState() !== Channel.STATE.CLOSED) { 54 | return; 55 | } 56 | this.setState(Channel.STATE.OPENING); 57 | 58 | var self = this; 59 | var socket = self.socket = new WebSocket(this.mesh.options.ws); 60 | 61 | socket.addEventListener('open', function(e) { 62 | self.setState(Channel.STATE.OPEN); 63 | // mark all attached channels as open 64 | for (var id in self.channels) { 65 | var channel = self.channels[id]; 66 | channel.setState(Channel.STATE.OPEN); 67 | channel.trigger('open', { 68 | data: ['open'] 69 | }); 70 | } 71 | }, false); 72 | 73 | socket.addEventListener('message', function(e) { 74 | var container; 75 | try { 76 | container = JSON.parse(e.data); 77 | } catch (err) { 78 | throw new Error('Invalid container received', container); 79 | } 80 | var from = container[0]; 81 | var data; 82 | try { 83 | data = JSON.parse(container[1]); 84 | } catch (err) { 85 | throw new Error('Invalid data received', data); 86 | } 87 | // one time upgrade of self id upon connection 88 | if (data[0] === 'connect') { 89 | // find a null self node and upgrade it 90 | var selfChannel = self.channels[null]; 91 | selfChannel.localNode.id = from; 92 | delete self.channels[null]; 93 | self.channels[from] = selfChannel; 94 | } 95 | var fromChannel = self.channels[from]; 96 | if (fromChannel) { 97 | // console.log('from', fromChannel.remoteNode.id, 98 | // 'to', fromChannel.localNode.id, data); 99 | fromChannel.trigger('message', { 100 | from: fromChannel.remoteNode.id, 101 | data: data 102 | }); 103 | } else { 104 | // console.warn('no from channel found', from); 105 | } 106 | }, false); 107 | 108 | socket.addEventListener('close', function() { 109 | self.setState(Channel.STATE.CLOSED); 110 | for (var id in self.channels) { 111 | var channel = self.channels[id]; 112 | channel.setState(Channel.STATE.CLOSED); 113 | channel.trigger('close', { 114 | from: channel.remoteNode.id, 115 | data: ['close'] 116 | }); 117 | } 118 | 119 | }, false); 120 | 121 | socket.addEventListener('error', function() { 122 | self.setState(Channel.STATE.CLOSED); 123 | for (var id in self.channels) { 124 | var channel = self.channels[id]; 125 | channel.setState(Channel.STATE.CLOSED); 126 | channel.trigger('error', { 127 | from: channel.remoteNode.id, 128 | data: ['error'] 129 | }); 130 | } 131 | }, false); 132 | 133 | }; 134 | 135 | SocketMultiplexer.prototype.close = function(channel) { 136 | this.unregister(channel); 137 | if (channel.remoteNode === channel.localNode.mesh.self) { 138 | this.setState(Channel.STATE.CLOSED); 139 | this.socket.close(); 140 | } 141 | // if (Object.keys(this.channels).length === 0) { 142 | // this.setState(Channel.STATE.CLOSED); 143 | // this.socket.close(); 144 | // } 145 | }; 146 | 147 | var debounce = function(func, wait, immediate) { 148 | var result; 149 | var timeout = null; 150 | return function() { 151 | var context = this, args = arguments; 152 | var later = function() { 153 | timeout = null; 154 | if (!immediate) result = func.apply(context, args); 155 | }; 156 | var callNow = immediate && !timeout; 157 | clearTimeout(timeout); 158 | timeout = setTimeout(later, wait); 159 | if (callNow) result = func.apply(context, args); 160 | return result; 161 | }; 162 | }; 163 | 164 | SocketMultiplexer._send = debounce(function() { 165 | 166 | }); 167 | 168 | SocketMultiplexer.prototype.send = function(channel, message) { 169 | // this.send._queuedMessages = this.send._queuedMessages || {}; 170 | // var targets = this.send._queuedMessages[message] || []; 171 | // targets.push(channel.remoteNode.id); 172 | // this.send._queuedMessages[message] = targets; 173 | 174 | // var self = this; 175 | // if (!this.send.sendInterval) { 176 | // this.send.sendInterval = setInterval(function() { 177 | // var msgs = Object.keys(self.send._queuedMessages); 178 | // if (msgs.length === 0) { 179 | // return; 180 | // } 181 | // var packets = []; 182 | // msgs.forEach(function(msg) { 183 | // var recipients = self.send._queuedMessages[msg]; 184 | // packets.push([recipients, msg]); 185 | // delete self.send._queuedMessages[msg]; 186 | // }); 187 | // var encoded = JSON.stringify(packets); 188 | // self.socket.send(encoded); 189 | // }, 100); 190 | // } 191 | 192 | // return true; 193 | 194 | // TODO: user defer to batch send messages 195 | var encoded = JSON.stringify([[channel.remoteNode.id], message]); 196 | return this.socket.send(encoded); 197 | }; 198 | 199 | -------------------------------------------------------------------------------- /tailbone/mesh/js/StateDrive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Doug Fritz dougfritz@google.com 3 | * @author Maciej Zasada maciej@unit9.com 4 | * Date: 6/2/13 5 | * Time: 11:04 PM 6 | */ 7 | 8 | /** 9 | * StateDrive 10 | * @constructor 11 | */ 12 | var StateDrive = function () { 13 | 14 | EventDispatcher.call(this); 15 | 16 | this._state = 0; 17 | this._callQueue = {}; 18 | 19 | }; 20 | 21 | /** 22 | * Extend EventDispatcher 23 | * @type {EventDispatcher} 24 | */ 25 | StateDrive.prototype = new EventDispatcher(); 26 | 27 | /** 28 | * Gets current object state 29 | * @returns {int} current state 30 | */ 31 | StateDrive.prototype.getState = function () { 32 | 33 | return this._state; 34 | 35 | }; 36 | 37 | /** 38 | * Sets current object state 39 | * @param state {int} new state 40 | */ 41 | StateDrive.prototype.setState = function (state) { 42 | 43 | this._state = state; 44 | this._executeQueuedCalls(); 45 | 46 | }; 47 | 48 | /** 49 | * Specifies minimum state for the member function to execute. 50 | * A function call of name 'funcName' will be delayed until the instance reaches given 'state'. 51 | * @param funcName {string} name of member function to set minimum call state for 52 | * @param validators... {RegExp...} optional validators per argument passed to future function call 53 | * @param state {int} minimum state value for the member function to execute 54 | */ 55 | StateDrive.prototype.setMinCallState = function (funcName, validators, state) { 56 | 57 | var originalFunction = this[funcName], 58 | stateId = arguments[arguments.length - 1], 59 | argumentValidators = arguments.length > 2 ? Array.prototype.slice.apply(arguments).slice(1, arguments.length - 1) : [], 60 | i; 61 | 62 | this[funcName] = function () { 63 | if (this._state >= stateId || arguments.length < argumentValidators.length) { 64 | return originalFunction.apply(this, arguments); 65 | } else { 66 | for (i = 0; i < argumentValidators.length; ++i) { 67 | if (!arguments[i].match(argumentValidators[i])) { 68 | return originalFunction.apply(this, arguments); 69 | } 70 | } 71 | return this._queueCall(funcName, arguments, stateId); 72 | } 73 | }; 74 | 75 | }; 76 | 77 | /** 78 | * Queues function call until a given minimum state is reached. 79 | * @param funcName {string} name of member function to queue 80 | * @param args {array} arguments to pass 81 | * @param state {int} minimum state 82 | */ 83 | StateDrive.prototype._queueCall = function (funcName, args, state) { 84 | 85 | this._callQueue[state] = this._callQueue[state] || []; 86 | this._callQueue[state].push({name: funcName, args: args}); 87 | 88 | }; 89 | 90 | /** 91 | * Executes queued calls for current and lower states 92 | */ 93 | StateDrive.prototype._executeQueuedCalls = function () { 94 | 95 | var i, j; 96 | for (i = 0; i <= this._state; ++i) { 97 | if (this._callQueue[i]) { 98 | for (j = 0; j < this._callQueue[i].length; ++j) { 99 | this[this._callQueue[i][j].name].apply(this, this._callQueue[i][j].args); 100 | } 101 | this._callQueue[i] = []; 102 | } 103 | } 104 | 105 | }; 106 | -------------------------------------------------------------------------------- /tailbone/mesh/websocket.js: -------------------------------------------------------------------------------- 1 | var port = parseInt(process.argv[2] || '8889') 2 | , WebSocketServer = require('ws').Server 3 | , http = require('http'); 4 | 5 | 6 | var app = http.createServer(function(req, res) { 7 | res.writeHead(200, {'Content-Type': 'text/plain'}); 8 | res.end(''+_count); 9 | }); 10 | 11 | var wss = new WebSocketServer({server: app}); 12 | 13 | var _count = 0; 14 | var _global_id = 0; 15 | function generate_id() { 16 | return ++_global_id; 17 | } 18 | 19 | var room_to_peers = {}; 20 | var id_to_peer = {}; 21 | 22 | wss.on('connection', function(ws) { 23 | _count++; 24 | ws.id = generate_id(); 25 | id_to_peer[ws.id] = ws; 26 | var room = ws.upgradeReq.url.substring(1); 27 | var peers = room_to_peers[room] || []; 28 | var connect = peers.map(function(p) { return p.id; }); 29 | connect.unshift('connect'); 30 | try { 31 | ws.send(JSON.stringify([ws.id, JSON.stringify(connect)])); 32 | } catch(e) {} 33 | peers.forEach(function(peer) { 34 | try { 35 | peer.send(JSON.stringify([peer.id, JSON.stringify(['enter', ws.id])])); 36 | } catch(e) {} 37 | }); 38 | peers.push(ws); 39 | room_to_peers[room] = peers; 40 | ws.on('message', function(message) { 41 | function forward(targets, payload) { 42 | if (!(targets instanceof Array)) { 43 | console.error('targets not Array.', targets, payload); 44 | return; 45 | } 46 | targets.forEach(function(target_id) { 47 | var target = id_to_peer[target_id]; 48 | if (target) { 49 | try { 50 | target.send(JSON.stringify([ws.id, payload])) 51 | } catch (e) {} 52 | } else { 53 | console.error('target not found.', target_id); 54 | } 55 | }); 56 | } 57 | var msg; 58 | try { 59 | msg = JSON.parse(message); 60 | } catch (e) { 61 | console.error(e); 62 | return; 63 | } 64 | if (!(msg instanceof Array && msg.length > 0)) { 65 | console.error('msg not correctly formated.', msg); 66 | return; 67 | } 68 | var targets = msg[0]; 69 | if (targets[0] instanceof Array) { 70 | msg.forEach(function(m) { 71 | if (!(m instanceof Array && m.length > 0)) { 72 | console.error('m not correctly formated.', m); 73 | return; 74 | } 75 | forward(m[0], m[1]); 76 | }); 77 | } else { 78 | forward(targets, msg[1]); 79 | } 80 | }); 81 | ws.on('close', function() { 82 | _count--; 83 | peers.splice(peers.indexOf(ws), 1); 84 | delete id_to_peer[ws.id]; 85 | peers.forEach(function(peer) { 86 | try { 87 | peer.send(JSON.stringify([peer.id, JSON.stringify(['leave', ws.id])])); 88 | } catch (e) {} 89 | }) 90 | }); 91 | }); 92 | 93 | 94 | app.listen(port); 95 | -------------------------------------------------------------------------------- /tailbone/mesh/websocket.py: -------------------------------------------------------------------------------- 1 | ## 2 | # @author Doug Fritz dougfritz@google.com 3 | # @author Maciej Zasada maciej@unit9.com 4 | ## 5 | 6 | import json 7 | import time 8 | from optparse import OptionParser 9 | import logging 10 | import tornado.httpserver 11 | import tornado.websocket 12 | import tornado.ioloop 13 | import tornado.web 14 | 15 | node_id_seed = 1 16 | nodes = [] 17 | nodes_by_id = {} 18 | meshes_by_id = {} 19 | mesh_id_by_node = {} 20 | 21 | 22 | def enter(node, mesh_id): 23 | """Joins a node to a mesh. Creates new mesh if needed.""" 24 | node.id = new_node_id() 25 | node.is_initiator_by_peer_node_id = {} 26 | nodes.append(node) 27 | nodes_by_id[node.id] = node 28 | 29 | if not mesh_id in meshes_by_id: 30 | meshes_by_id[mesh_id] = [] 31 | mesh = meshes_by_id[mesh_id] 32 | mesh.append(node) 33 | mesh_id_by_node[node] = mesh_id 34 | 35 | logging.debug('enter (node ID: %s, mesh ID: %s)' % (node.id, mesh_id)) 36 | # exist should be the first thing sent 37 | send_to_node(node, node, json.dumps(['connect'] + get_exist(mesh, node.id))) 38 | # make the enter call be a self message for routing 39 | msg = json.dumps(['enter', node.id]) 40 | for n in mesh: 41 | if n != node: 42 | send_to_node(n, n, msg) 43 | 44 | # send_to_mesh(mesh, node, ['enter', node.id]) 45 | return True 46 | 47 | 48 | def leave(node): 49 | """Removes node from meshes, disconnects node.""" 50 | if not node: 51 | return 52 | mesh_id = mesh_id_by_node[node] 53 | mesh = meshes_by_id[mesh_id] 54 | del mesh_id_by_node[node] 55 | mesh.remove(node) 56 | if len(mesh) == 0: 57 | del meshes_by_id[mesh_id] 58 | else: 59 | msg = json.dumps(['leave', node.id]) 60 | for n in mesh: 61 | if n != node: 62 | send_to_node(n, n, msg) 63 | nodes.remove(node) 64 | del nodes_by_id[node.id] 65 | try: 66 | node.close() 67 | except: 68 | pass 69 | logging.debug('leave (node ID: %s, mesh ID: %s)' % (node.id, mesh_id)) 70 | 71 | 72 | def parse_message(node, message): 73 | """Interprets node message and directs it forward.""" 74 | mesh_id = mesh_id_by_node[node] 75 | message_object = None 76 | to_nodes = None 77 | message_data = None 78 | print 'received %s (node ID: %s, mesh ID: %s)' % (message, node.id, mesh_id) 79 | try: 80 | message_object = json.loads(message) 81 | to_nodes = message_object[0] 82 | # check if it is a list of messages 83 | if type(to_nodes[0]) is list: 84 | for msg in message_object: 85 | to_nodes, message_data = msg 86 | send_to_node_ids(to_nodes, node, message_data) 87 | return 88 | message_data = message_object[1] 89 | except AttributeError as e: 90 | print e 91 | return 92 | send_to_node_ids(to_nodes, node, message_data) 93 | 94 | 95 | def wrap_message(message, sender_node): 96 | """Wraps message with sender ID and timestamp.""" 97 | try: 98 | message_string = json.dumps([sender_node.id, time.time(), message]) 99 | return message_string 100 | except: 101 | return None 102 | 103 | 104 | def send_to_node(node, sender_node, message): 105 | """Sends message to a node.""" 106 | message_string = wrap_message(message, sender_node) 107 | if message_string: 108 | logging.info('sending to node %s (node ID: %s, to ID: %s)' % (message_string, sender_node.id, node.id)) 109 | try: 110 | node.write_message(message_string) 111 | except: 112 | pass 113 | 114 | 115 | def send_to_node_ids(node_ids, sender_node, message): 116 | """Sends message to array of nodes.""" 117 | for node_id in node_ids: 118 | if node_id in nodes_by_id: 119 | send_to_node(nodes_by_id[node_id], sender_node, message) 120 | 121 | 122 | def send_to_mesh(mesh, sender_node, message): 123 | """Sends message to a mesh.""" 124 | message_string = wrap_message(message, sender_node) 125 | if message_string: 126 | logging.info('sending to mesh %s (node ID: %s, mesh ID: *)' % (message_string, sender_node.id)) 127 | for node in mesh: 128 | if node != sender_node: 129 | try: 130 | node.write_message(message_string) 131 | except: 132 | pass 133 | else: 134 | logging.warning('invalid message format %s' % message) 135 | 136 | 137 | def get_exist(mesh, ignore): 138 | """Gets a list of connected node IDs by mesh.""" 139 | return [node.id for node in mesh if node.id != ignore] 140 | 141 | 142 | def new_node_id(): 143 | """Generates new node ID.""" 144 | global node_id_seed 145 | node_id = node_id_seed 146 | node_id_seed = node_id_seed + 1 147 | return node_id 148 | 149 | 150 | class Handler(tornado.websocket.WebSocketHandler): 151 | """WebSocket connection and message handler.""" 152 | 153 | def open(self): 154 | enter(self, self.request.path[1:]) 155 | 156 | def on_message(self, message): 157 | parse_message(self, message) 158 | 159 | def on_close(self): 160 | leave(self) 161 | 162 | 163 | def main(): 164 | """Instantiates WebSocket server Usage: TODO: add usage notes""" 165 | parser = OptionParser() 166 | parser.add_option('-d', '--debug', dest='debug', action='store_true', help='enables debug mode', metavar='DEBUG', default=False) 167 | parser.add_option('-p', '--port', dest='port', help='port number to run the server on', metavar='PORT', default=2345) 168 | parser.add_option('-u', '--url', dest='url', help='base URL for connections', metavar='URL', default='') 169 | parser.add_option('-r', '--report', dest='report', help='URL to report load to', metavar='REPORT', default='') 170 | (options, args) = parser.parse_args() 171 | 172 | server = tornado.httpserver.HTTPServer(tornado.web.Application([(options.url + '/.*', Handler)])) 173 | logging.getLogger().setLevel(logging.DEBUG if options.debug else logging.INFO) 174 | server.listen(options.port) 175 | logging.debug('starting server on port %s' % options.port) 176 | tornado.ioloop.IOLoop.instance().start() 177 | 178 | if __name__ == "__main__": 179 | main() 180 | -------------------------------------------------------------------------------- /tailbone/pathrewrite/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Rewrites any path to the index.html file for html5mode history and location. 16 | 17 | # shared resources and global variables 18 | from tailbone import DEBUG 19 | from tailbone.static.protected import _config 20 | 21 | import webapp2 22 | import yaml 23 | 24 | # index.html is symlinked to api/client/index.html 25 | index = None 26 | with open("tailbone/pathrewrite/index.html") as f: 27 | index = f.read() 28 | 29 | is_protected = False 30 | with open("app.yaml") as f: 31 | appyaml = yaml.load(f) 32 | includes = [i for i in appyaml.get("includes", [])] 33 | is_protected = "tailbone/static/protected" in includes 34 | if is_protected: 35 | path = "client/" + _config.BASE_PATH + "/index.html" 36 | with open(path) as f: 37 | index = f.read() 38 | 39 | 40 | # Pathrewrite Handler 41 | # ------------ 42 | # 43 | # Proxies any page to the base url 44 | class PathrewriteHandler(webapp2.RequestHandler): 45 | def get(self): 46 | if is_protected: 47 | authorized = _config.is_authorized(self.request) 48 | if not authorized: 49 | self.response.out.write( 50 | _config.unauthorized_response(self.request)) 51 | return 52 | self.response.out.write(index) 53 | 54 | app = webapp2.WSGIApplication([ 55 | (r"^[^.]*$", PathrewriteHandler), 56 | ], debug=DEBUG) 57 | -------------------------------------------------------------------------------- /tailbone/pathrewrite/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | # - url: /((?!api).)[^.]* 17 | # script: tailbone.pathrewrite.app 18 | - url: /[^.]* 19 | script: tailbone.pathrewrite.app 20 | 21 | -------------------------------------------------------------------------------- /tailbone/pathrewrite/index.html: -------------------------------------------------------------------------------- 1 | ../../client/app/index.html -------------------------------------------------------------------------------- /tailbone/proxy/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import urllib 16 | import webapp2 17 | from tailbone import DEBUG 18 | 19 | from google.appengine.api import urlfetch 20 | from google.appengine.api import lib_config 21 | 22 | 23 | class _ConfigDefaults(object): 24 | # list of valid domain to restrict proxy to defaults to anything 25 | RESTRICTED_DOMAINS = None 26 | 27 | _config = lib_config.register('tailboneProxy', _ConfigDefaults.__dict__) 28 | 29 | 30 | # Simple Proxy Server 31 | # --------------------- 32 | # 33 | # Simple proxy server should you need it. Comment out the code in app.yaml to enable. 34 | # 35 | class ProxyHandler(webapp2.RequestHandler): 36 | def proxy(self, *args, **kwargs): 37 | url = urllib.unquote(self.request.get('url')) 38 | # TODO: check agains RESTRICTED_DOMAINS 39 | if url: 40 | if _config.RESTRICTED_DOMAINS: 41 | if url not in _config.RESTRICTED_DOMAINS: 42 | self.error(500) 43 | self.response.out.write("Restricted domain.") 44 | return 45 | resp = urlfetch.fetch(url, method=self.request.method, headers=self.request.headers) 46 | for k,v in resp.headers.iteritems(): 47 | self.response.headers[k] = v 48 | self.response.status = resp.status_code 49 | self.response.out.write(resp.content) 50 | else: 51 | self.error(500) 52 | self.response.out.write("Must provide a 'url' parameter.") 53 | def get(self, *args, **kwargs): 54 | self.proxy(*args, **kwargs) 55 | def put(self, *args, **kwargs): 56 | self.proxy(*args, **kwargs) 57 | def post(self, *args, **kwargs): 58 | self.proxy(*args, **kwargs) 59 | def delete(self, *args, **kwargs): 60 | self.proxy(*args, **kwargs) 61 | 62 | 63 | app = webapp2.WSGIApplication([ 64 | (r".*", ProxyHandler), 65 | ], debug=DEBUG) 66 | 67 | -------------------------------------------------------------------------------- /tailbone/proxy/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/proxy/?.* 17 | script: tailbone.proxy.app 18 | -------------------------------------------------------------------------------- /tailbone/restful/counter.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from google.appengine.api import memcache 4 | from google.appengine.ext import ndb 5 | 6 | 7 | SHARD_KEY_TEMPLATE = 'shard-{}-{:d}' 8 | 9 | 10 | class TailboneGeneralCounterShardConfig(ndb.Model): 11 | """Tracks the number of shards for each named counter.""" 12 | num_shards = ndb.IntegerProperty(default=20) 13 | 14 | @classmethod 15 | def all_keys(cls, name): 16 | """Returns all possible keys for the counter name given the config. 17 | 18 | Args: 19 | name: The name of the counter. 20 | 21 | Returns: 22 | The full list of ndb.Key values corresponding to all the possible 23 | counter shards that could exist. 24 | """ 25 | config = cls.get_or_insert(name) 26 | shard_key_strings = [SHARD_KEY_TEMPLATE.format(name, index) 27 | for index in range(config.num_shards)] 28 | return [ndb.Key(TailboneGeneralCounterShard, shard_key_string) 29 | for shard_key_string in shard_key_strings] 30 | 31 | 32 | class TailboneGeneralCounterShard(ndb.Model): 33 | """Shards for each named counter.""" 34 | count = ndb.IntegerProperty(default=0) 35 | 36 | 37 | def get_count(name): 38 | """Retrieve the value for a given sharded counter. 39 | 40 | Args: 41 | name: The name of the counter. 42 | 43 | Returns: 44 | Integer; the cumulative count of all sharded counters for the given 45 | counter name. 46 | """ 47 | total = memcache.get(name) 48 | if total is None: 49 | total = 0 50 | all_keys = TailboneGeneralCounterShardConfig.all_keys(name) 51 | for counter in ndb.get_multi(all_keys): 52 | if counter is not None: 53 | total += counter.count 54 | memcache.add(name, total, 60) 55 | return total 56 | 57 | 58 | def decrement(name): 59 | """Decrement the value for a given sharded counter. 60 | 61 | Args: 62 | name: The name of the counter. 63 | """ 64 | config = TailboneGeneralCounterShardConfig.get_or_insert(name) 65 | _decrement(name, config.num_shards) 66 | 67 | 68 | @ndb.transactional 69 | def _decrement(name, num_shards): 70 | """Transactional helper to decrement the value for a given sharded counter. 71 | 72 | Also takes a number of shards to determine which shard will be used. 73 | 74 | Args: 75 | name: The name of the counter. 76 | num_shards: How many shards to use. 77 | """ 78 | index = random.randint(0, num_shards - 1) 79 | shard_key_string = SHARD_KEY_TEMPLATE.format(name, index) 80 | counter = TailboneGeneralCounterShard.get_by_id(shard_key_string) 81 | if counter is None: 82 | counter = TailboneGeneralCounterShard(id=shard_key_string) 83 | counter.count -= 1 84 | counter.put() 85 | # Memcache increment does nothing if the name is not a key in memcache 86 | memcache.decr(name) 87 | 88 | 89 | def increment(name): 90 | """Increment the value for a given sharded counter. 91 | 92 | Args: 93 | name: The name of the counter. 94 | """ 95 | config = TailboneGeneralCounterShardConfig.get_or_insert(name) 96 | _increment(name, config.num_shards) 97 | 98 | 99 | @ndb.transactional 100 | def _increment(name, num_shards): 101 | """Transactional helper to increment the value for a given sharded counter. 102 | 103 | Also takes a number of shards to determine which shard will be used. 104 | 105 | Args: 106 | name: The name of the counter. 107 | num_shards: How many shards to use. 108 | """ 109 | index = random.randint(0, num_shards - 1) 110 | shard_key_string = SHARD_KEY_TEMPLATE.format(name, index) 111 | counter = TailboneGeneralCounterShard.get_by_id(shard_key_string) 112 | if counter is None: 113 | counter = TailboneGeneralCounterShard(id=shard_key_string) 114 | counter.count += 1 115 | counter.put() 116 | # Memcache increment does nothing if the name is not a key in memcache 117 | memcache.incr(name) 118 | 119 | 120 | @ndb.transactional 121 | def increase_shards(name, num_shards): 122 | """Increase the number of shards for a given sharded counter. 123 | 124 | Will never decrease the number of shards. 125 | 126 | Args: 127 | name: The name of the counter. 128 | num_shards: How many shards to use. 129 | """ 130 | config = TailboneGeneralCounterShardConfig.get_or_insert(name) 131 | if config.num_shards < num_shards: 132 | config.num_shards = num_shards 133 | config.put() 134 | -------------------------------------------------------------------------------- /tailbone/restful/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | # TODO: App Engine doesn't seem to accept multiple word negative lookahead 17 | # https://code.google.com/p/googleappengine/issues/detail?id=541 18 | # - url: /api/((?!admin|events|files|js|pathrewrite|proxy|search).)/?.* 19 | # script: tailbone.restful.app 20 | - url: /api/?.* 21 | script: tailbone.restful.app 22 | -------------------------------------------------------------------------------- /tailbone/restful/models.js: -------------------------------------------------------------------------------- 1 | // This is a simple javascript wrapper to make using the restful api provided 2 | // by tailbone easier. It also includes bi-directional data binding with 3 | // AppEngine and the channel api so that your javascript models upload in 4 | // real time. 5 | 6 | 7 | // Quering and Filtering 8 | // --------------------- 9 | // These are some useful helper functions for constructing queries, although you 10 | // probably won't have to use these directly, see the query section below. 11 | 12 | 13 | // Emulate legacy getter/setter API using ES5 APIs. 14 | // Since __defineGetter__ and __defineSetter__ are not supported any longer by IE9 or 10 or Windows 8, and Box2D for javascript v2.1a still uses them, this shim is required to run Box2D in those environments. 15 | // This is taken directly from Allen Wirfs-Brock's blog at: 16 | // http://blogs.msdn.com/b/ie/archive/2010/09/07/transitioning-existing-code-to-the-es5-getter-setter-apis.aspx 17 | try { 18 | if(!Object.prototype.__defineGetter__ && 19 | Object.defineProperty({}, "x", { get: function() { return true } }).x) { 20 | Object.defineProperty(Object.prototype, "__defineGetter__", 21 | { enumerable: false, configurable: true, 22 | value: function(name, func) { 23 | Object.defineProperty(this, name, 24 | { get: func, enumerable: true, configurable: true }); 25 | } 26 | }); 27 | Object.defineProperty(Object.prototype, "__defineSetter__", 28 | { enumerable: false, configurable: true, 29 | value: function(name, func) { 30 | Object.defineProperty(this, name, 31 | { set: func, enumerable: true, configurable: true }); 32 | } 33 | }); 34 | } 35 | } catch(defPropException) { /*Do nothing if an exception occurs*/ }; 36 | //////////////////////// 37 | 38 | function FILTER(name, opsymbol, value) { 39 | return [name, opsymbol, value]; 40 | } 41 | 42 | function ORDER(name) { 43 | return name; 44 | } 45 | 46 | function AND() { 47 | var f = ['AND']; 48 | for (var i = 0; i < arguments.length; i++) { 49 | f.push(arguments[i]); 50 | } 51 | return f; 52 | } 53 | 54 | function OR() { 55 | var f = ['OR']; 56 | for (var i = 0; i < arguments.length; i++) { 57 | f.push(arguments[i]); 58 | } 59 | return f; 60 | } 61 | 62 | 63 | // This helps construct a model type which can be queried and created. 64 | var Model = function(type, opt_schema) { 65 | var ignored_prefixes = ['_', '$']; 66 | 67 | 68 | // Model 69 | // ----- 70 | // This is a model class for the particular type. This allows you to work with 71 | // your models directly in javascript and has several built in functions to 72 | // save them back to the server. 73 | var Model = function(defaults) { 74 | populate(this, defaults); 75 | }; 76 | 77 | // Get a model by its id. 78 | Model.get = function(id, opt_callback, opt_error, opt_recurse) { 79 | var m = new Model(); 80 | m.Id = id; 81 | m.$update(opt_callback, opt_error, opt_recurse); 82 | return m; 83 | }; 84 | 85 | // Query generates a iterator for a query object. Queries inherit from the 86 | // native array type so all the ES5 iterators work on query objects, for 87 | // example: 88 | // 89 | // var Todo = new tailbone.Model("todos"); 90 | // Todo.query(function(todos) { 91 | // todos.forEach(function(v,i) { 92 | // console.log(v,i); 93 | // }); 94 | // }); 95 | Model.query = function(opt_callback) { 96 | var query = new Query(); 97 | // xhr query for collection with timeout to allow for chaining. 98 | // 99 | // var results = Todo.query().filter("text =", "sample") 100 | query._queued = setTimeout(function() { 101 | query.fetch(opt_callback); 102 | }, 0); 103 | // Bind to watch for changes to this model by others on the server. 104 | if (tailbone.databinding) { 105 | tailbone.bind(type, function() { query.fetch(); }); 106 | } 107 | return query; 108 | }; 109 | 110 | // Helper function to serialize a model and strip any properties that match 111 | // the set of ignored prefixes or are of an unsupported type such as a 112 | // function. 113 | function serializeModel(model, submodel) { 114 | if (model instanceof Array) { 115 | var items = []; 116 | for(var i=0;i 36 | You must be an approved logged in user. 37 | Login In 38 | 39 | """ % (request.path, qs) 40 | 41 | # Example of password auth 42 | class _ConfigDefaults(object): 43 | BASE_PATH = "app" 44 | PASSWORD = "notasecret" 45 | 46 | def is_authorized(request): 47 | return _config.PASSWORD == request.cookies.get("whisper") 48 | 49 | def unauthorized_response(request): 50 | return """ 51 | 52 | 53 | 67 | 68 | Please do not share 69 |

Authentication:

70 |
71 | 72 | 73 |
74 | 75 | """ 76 | 77 | _config = lib_config.register('tailboneStaticProtected', _ConfigDefaults.__dict__) 78 | 79 | mimetypes.add_type("image/svg+xml", ".svg") 80 | mimetypes.add_type("application/x-font-otf", ".otf") 81 | mimetypes.add_type("application/font-woff", ".woff") 82 | mimetypes.add_type("application/x-font-ttf", ".ttf") 83 | mimetypes.add_type("application/vnd.ms-fontobject", ".eot") 84 | 85 | 86 | class ProtectedHandler(webapp2.RequestHandler): 87 | def proxy(self, *args, **kwargs): 88 | authorized = _config.is_authorized(self.request) 89 | if not authorized: 90 | self.response.out.write( 91 | _config.unauthorized_response(self.request)) 92 | return 93 | path = self.request.path 94 | path = "client/" + _config.BASE_PATH + path 95 | if path[-1] == "/": 96 | path += "index.html" 97 | mimetype, _ = mimetypes.guess_type(path) 98 | if mimetype: 99 | self.response.headers["Content-Type"] = mimetype 100 | try: 101 | with open(urllib.unquote(path)) as f: 102 | self.response.out.write(f.read()) 103 | except IOError: 104 | self.error(404) 105 | def get(self, *args, **kwargs): 106 | self.proxy(*args, **kwargs) 107 | def put(self, *args, **kwargs): 108 | self.proxy(*args, **kwargs) 109 | def post(self, *args, **kwargs): 110 | self.proxy(*args, **kwargs) 111 | def delete(self, *args, **kwargs): 112 | self.proxy(*args, **kwargs) 113 | 114 | 115 | app = webapp2.WSGIApplication([ 116 | (r".*", ProtectedHandler), 117 | ], debug=DEBUG) 118 | 119 | -------------------------------------------------------------------------------- /tailbone/static/protected/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: (/?[^.]*)[^/] 17 | script: tailbone.add_slash 18 | - url: .* 19 | script: tailbone.static.protected.app 20 | -------------------------------------------------------------------------------- /tailbone/static/public/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/static/public/__init__.py -------------------------------------------------------------------------------- /tailbone/static/public/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: (/?[^.]*)[^/] 17 | script: tailbone.add_slash 18 | - url: (/?.*)/ 19 | static_files: client/app\1/index.html 20 | upload: client/app(.*)/index.html 21 | - url: / 22 | static_dir: client/app -------------------------------------------------------------------------------- /tailbone/test/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # shared resources and global variables 16 | from tailbone import DEBUG 17 | 18 | import webapp2 19 | 20 | NONMUTATING = ["clocksync"] 21 | 22 | 23 | # Test Handler 24 | # ------------ 25 | # 26 | # QUnit tests can only be preformed on the local host because they actively modify the database and 27 | # don't properly clean up after themselves yet. 28 | class TestHandler(webapp2.RequestHandler): 29 | def get(self, path): 30 | if DEBUG or path in NONMUTATING: 31 | try: 32 | with open("tailbone/test/{}.html".format(path)) as f: 33 | self.response.out.write(f.read()) 34 | except: 35 | self.response.out.write("No such test found.") 36 | else: 37 | self.response.out.write("Sorry, most tests can only be run from localhost because they modify the \ 38 | datastore.") 39 | 40 | app = webapp2.WSGIApplication([ 41 | (r"/test/?(.*)", TestHandler), 42 | ], debug=DEBUG) 43 | -------------------------------------------------------------------------------- /tailbone/test/auth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 41 | 42 | -------------------------------------------------------------------------------- /tailbone/test/clocksync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tailbone/test/events.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/test/events.html -------------------------------------------------------------------------------- /tailbone/test/extras/qunit-git.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0pre-24a32cf1e9a83acccc3e5de101253dff8bea1183 2013-05-05 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /tailbone/test/files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 132 | 133 | -------------------------------------------------------------------------------- /tailbone/test/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /test/extras 17 | static_dir: tailbone/test/extras 18 | - url: /test/?(.*) 19 | script: tailbone.test.app 20 | -------------------------------------------------------------------------------- /tailbone/test/mesh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /tailbone/test/messages.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/test/messages.html -------------------------------------------------------------------------------- /tailbone/test/metadata.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 64 | 65 | -------------------------------------------------------------------------------- /tailbone/test/proxy.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/test/proxy.html -------------------------------------------------------------------------------- /tailbone/test/search.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dataarts/tailbone/5ed162bbfdb369a31f4f50bd089239c9c105457a/tailbone/test/search.html -------------------------------------------------------------------------------- /tailbone/turn/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from tailbone import BaseHandler 16 | from tailbone import as_json 17 | from tailbone import AppError 18 | from tailbone import DEBUG 19 | from tailbone import PREFIX 20 | from tailbone.compute_engine import LoadBalancer 21 | from tailbone.compute_engine import TailboneCEInstance 22 | from tailbone.compute_engine import STARTUP_SCRIPT_BASE 23 | 24 | import binascii 25 | from hashlib import sha1 26 | import hmac 27 | import md5 28 | import time 29 | import webapp2 30 | 31 | from google.appengine.api import lib_config 32 | from google.appengine.ext import ndb 33 | 34 | 35 | class _ConfigDefaults(object): 36 | SECRET = "notasecret" 37 | REALM = "localhost" 38 | RESTRICTED_DOMAINS = ["localhost"] 39 | SOURCE_SNAPSHOT = None 40 | PARAMS = {} 41 | 42 | _config = lib_config.register('tailboneTurn', _ConfigDefaults.__dict__) 43 | 44 | # Prefixing internal models with Tailbone to avoid clobbering when using RESTful API 45 | class TailboneTurnInstance(TailboneCEInstance): 46 | SOURCE_SNAPSHOT = _config.SOURCE_SNAPSHOT 47 | PARAMS = dict(dict(TailboneCEInstance.PARAMS, **{ 48 | "name": "turn-id", 49 | "metadata": { 50 | "items": [ 51 | { 52 | "key": "startup-script", 53 | "value": STARTUP_SCRIPT_BASE + """ 54 | # load turnserver 55 | cd /var/run 56 | ulimit -c 99999999 57 | cd / 58 | curl -O http://turnserver.open-sys.org/downloads/v3.2.3.6/turnserver-3.2.3.6-debian-wheezy-ubuntu-mint-x86-64bits.tar.gz 59 | tar xvfz turnserver-3.2.3.6-debian-wheezy-ubuntu-mint-x86-64bits.tar.gz 60 | dpkg -i rfc5766-turn-server_3.2.3.6-1_amd64.deb 61 | apt-get -fy install 62 | IP=$(gcutil getinstance $(hostname) 2>&1 | grep '^| *external-ip *| ' | grep -oEi "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}") 63 | while true 64 | do 65 | turnserver --use-auth-secret -v -a -X $IP -f --static-auth-secret %s -r %s 66 | sleep 1 67 | done 68 | 69 | """ % (_config.SECRET, _config.REALM) 70 | }, 71 | ], 72 | } 73 | }), **_config.PARAMS) 74 | 75 | secret = ndb.StringProperty(default=_config.SECRET) 76 | 77 | def credentials(username, secret=None): 78 | timestamp = str(time.mktime(time.gmtime()) + 24 * 3600).split('.')[0] 79 | username = "{}:{}".format(timestamp, username) 80 | if not secret: 81 | secret = _config.SECRET 82 | # force string 83 | secret = str(secret) 84 | password = hmac.new(secret, username, sha1) 85 | password = binascii.b2a_base64(password.digest())[:-1] 86 | return username, password 87 | 88 | 89 | class TurnHandler(BaseHandler): 90 | @as_json 91 | def get(self): 92 | if _config.RESTRICTED_DOMAINS: 93 | if self.request.host_url not in _config.RESTRICTED_DOMAINS: 94 | raise AppError("Invalid host.") 95 | username = self.request.get("username") 96 | if not username: 97 | raise AppError("Must provide username.") 98 | instance = LoadBalancer.find(TailboneTurnInstance) 99 | if not instance: 100 | raise AppError('Instance not found, try again later.') 101 | username, password = credentials(username, instance.secret) 102 | return { 103 | "username": username, 104 | "password": password, 105 | "uris": [ 106 | "turn:{}:3478?transport=udp".format(instance.address), 107 | "turn:{}:3478?transport=tcp".format(instance.address) 108 | ], 109 | } 110 | 111 | def post(self): 112 | return self.get() 113 | 114 | app = webapp2.WSGIApplication([ 115 | (r"{}turn/?.*".format(PREFIX), TurnHandler), 116 | ], debug=DEBUG) 117 | -------------------------------------------------------------------------------- /tailbone/turn/include.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2013 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | handlers: 16 | - url: /api/turn/?.* 17 | script: tailbone.turn.app -------------------------------------------------------------------------------- /validation.json: -------------------------------------------------------------------------------- 1 | ../validation.json -------------------------------------------------------------------------------- /validation.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": { 3 | "skipvalidation": "", 4 | "anything": ".*", 5 | "shortstring": "^.{3,30}$", 6 | "integer": "^[-]?[0-9]+$", 7 | "float": "^[-]?([0-9]*\\.[0-9]+|[0-9]+)$", 8 | "timestamp": "^[0-9]+$", 9 | "object": "^\\{.*\\}$", 10 | "objectdeep": { 11 | "anything": ".*", 12 | "skipvalidation": "", 13 | "integer": "^[-]?[0-9]+$" 14 | }, 15 | "list": "^\\[.*\\]$" 16 | }, 17 | "documents_with_anything": "" 18 | } 19 | --------------------------------------------------------------------------------