├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── boot.rb ├── config.ru ├── db └── migrations │ └── 001_initial_schema.rb ├── lib ├── tentd.rb └── tentd │ ├── api.rb │ ├── api │ ├── cors_headers.rb │ ├── meta_profile.rb │ ├── middleware.rb │ ├── middleware │ │ ├── attachment_redirect.rb │ │ ├── authentication.rb │ │ ├── authorization.rb │ │ ├── authorize_get_entity.rb │ │ ├── create_post.rb │ │ ├── create_post_version.rb │ │ ├── delete_post.rb │ │ ├── discover.rb │ │ ├── get_attachment.rb │ │ ├── get_post.rb │ │ ├── hello_world.rb │ │ ├── list_post_children.rb │ │ ├── list_post_mentions.rb │ │ ├── list_post_versions.rb │ │ ├── lookup_post.rb │ │ ├── not_found.rb │ │ ├── parse_content_type.rb │ │ ├── parse_input_data.rb │ │ ├── parse_link_header.rb │ │ ├── posts_feed.rb │ │ ├── proxy_attachment_redirect.rb │ │ ├── proxy_post_list.rb │ │ ├── serve_post.rb │ │ ├── set_request_proxy_manager.rb │ │ ├── user_lookup.rb │ │ ├── validate_input_data.rb │ │ └── validate_post_content_type.rb │ ├── notification_importer.rb │ ├── oauth.rb │ ├── oauth │ │ ├── authorize.rb │ │ └── token.rb │ ├── relationship_initialization.rb │ └── serialize_response.rb │ ├── authorizer.rb │ ├── authorizer │ ├── auth_candidate.rb │ └── auth_candidate │ │ ├── app.rb │ │ ├── app_auth.rb │ │ ├── base.rb │ │ └── relationship.rb │ ├── feed.rb │ ├── feed │ └── pagination.rb │ ├── importer.rb │ ├── model.rb │ ├── models │ ├── app.rb │ ├── app_auth.rb │ ├── attachment │ │ ├── fog.rb │ │ └── sequel.rb │ ├── credentials.rb │ ├── delivery_failure.rb │ ├── entity.rb │ ├── large_object.rb │ ├── mention.rb │ ├── parent.rb │ ├── post.rb │ ├── post_builder.rb │ ├── posts_attachment.rb │ ├── refs.rb │ ├── relationship.rb │ ├── subscription.rb │ ├── type.rb │ └── user.rb │ ├── proxied_post.rb │ ├── query.rb │ ├── refs.rb │ ├── relationship_importer.rb │ ├── request_proxy_manager.rb │ ├── schema_validator.rb │ ├── schema_validator │ └── format_validators.rb │ ├── sequel │ └── plugins │ │ └── paranoia.rb │ ├── sidekiq.rb │ ├── tasks │ └── db.rb │ ├── utils.rb │ ├── version.rb │ ├── worker.rb │ └── worker │ ├── notification_app_deliverer.rb │ ├── notification_deliverer.rb │ ├── notification_dispatch.rb │ └── relationship_initiation.rb ├── sidekiq.rb └── tentd.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | sidekiq.log 19 | validator-sidekiq.log 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - psql -c 'create database tentd_test;' -U postgres 3 | - bundle exec rake db:migrate DATABASE_URL=postgres://postgres@localhost/tentd_test 4 | - psql -c 'create database validator_tentd_test;' -U postgres 5 | - bundle exec rake db:migrate DATABASE_URL=postgres://postgres@localhost/validator_tentd_test 6 | env: 7 | - TEST_DATABASE_URL=postgres://postgres@localhost/tentd_test TEST_VALIDATOR_TEND_DATABASE_URL=postgres://postgres@localhost/validator_tentd_test 8 | rvm: 9 | - 2.0.0 10 | - 1.9.3 11 | services: 12 | - redis-server 13 | notifications: 14 | email: false 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rack-putty', :git => 'git://github.com/tent/rack-putty.git', :branch => 'master' 6 | gem 'tent-client', :git => 'git://github.com/tent/tent-client-ruby.git', :branch => 'master' 7 | gem 'tent-schemas', :git => 'git://github.com/tent/tent-schemas.git', :branch => 'master' 8 | gem 'json-pointer', :git => 'git://github.com/tent/json-pointer-ruby.git', :branch => 'master' 9 | gem 'tent-canonical-json', :git => 'git://github.com/tent/tent-canonical-json-ruby.git', :branch => 'master' 10 | gem 'api-validator', :git => 'git://github.com/tent/api-validator.git', :branch => 'master' 11 | gem 'hawk-auth', :git => 'git://github.com/tent/hawk-ruby.git', :branch => 'master' 12 | gem 'sequel_pg', :require => 'sequel' 13 | gem 'sequel', :require => false 14 | 15 | group :development, :test do 16 | gem 'tent-validator', :git => 'git://github.com/tent/tent-validator.git', :branch => 'master' 17 | end 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tent.is, LLC. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Tent.is, LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tentd - Protocol v0.3 [![Build Status](https://travis-ci.org/tent/tentd.png?branch=master)](https://travis-ci.org/tent/tentd) 2 | 3 | **If you're looking to self-host, see [tentd-omnibus](https://github.com/tent/tentd-omnibus).** 4 | 5 | tentd is an alpha implementation of a Tent Protocol server. It currently contains **broken code, many bugs, and security flaws**. The code should only be used to experiment with how Tent works. Under no circumstances should the code in its current form be used for data that is supposed to be private. 6 | 7 | ## Setup 8 | 9 | ### ENV Variables 10 | 11 | name | required | description 12 | --------------- | -------- | ----------- 13 | TENT_ENTITY | Required | Entity URI (can be omitted if `env['current_user']` is set to an instance of `TentD::Model::User` prior to `TentD::API::UserLookup` being called) 14 | DATABASE_URL | Required | URL of postgres database (e.g. `postgres://localhost/tentd`) 15 | REDIS_URL | Required | URL of redis server (e.g. `redis://localhost:6379`) 16 | REDIS_NAMESPACE | Optional | Redis key namespace for sidekiq (defaults to not set) 17 | RUN_SIDEKIQ | Optional | Set to 'true' if you want to boot sidekiq via `config.ru` 18 | SIDEKIQ_LOG | Optional | Sidekiq log file (defaults to STDOUT and STDERR) 19 | SOFT_DELETE | Optional | To perminently delete db records, set to `false`. Defaults to `true` (sets `deleted_at` timestamp instead of removing from db) 20 | API_ROOT | Optional | Required if different from `TENT_ENTITY` 21 | SERVER_PORT | Optional | Exposed server port (e.g. 443) 22 | 23 | #### Attachment Storage Options 24 | 25 | Precedence is in the same order as listed below. 26 | 27 | name | env | description 28 | ---- | --- | ----------- 29 | Amazon S3 | AWS_ACCESS_KEY_ID | Access key identifier 30 | | AWS_SECRET_ACCESS_KEY | Access key 31 | | S3_BUCKET | Bucket name 32 | Google | GOOGLE_STORAGE_ACCESS_KEY_ID | Access key identifier 33 | | GOOGLE_STORAGE_SECRET_ACCESS_KEY | Access key 34 | | GOOGLE_BUCKET | Bucket name 35 | Rackspace | RACKSPACE_USERNAME | Username 36 | | RACKSPACE_API_KEY | Api key 37 | | RACKSPACE_AUTH_URL | Auth URL (European Rackspace) 38 | | RACKSPACE_CONTAINER | Container (bucket) name 39 | Filesystem | LOCAL_ATTACHMENTS_ROOT | Path to directory (e.g. `~/tent-attachments`) 40 | Postgres | POSTGRES_ATTACHMENTS | Default. Set to `true` to override any of the other options. 41 | 42 | ### Database Setup 43 | 44 | ```bash 45 | createdb tentd 46 | DATABASE_URL=postgres://localhost/tentd bundle exec rake db:migrate 47 | ``` 48 | 49 | ### Running Server 50 | 51 | ```bash 52 | bundle exec unicorn 53 | ``` 54 | 55 | ### Running Sidekiq 56 | 57 | ```bash 58 | bundle exec sidekiq -r ./sidekiq.rb 59 | ``` 60 | 61 | or 62 | 63 | ```bash 64 | RUN_SIDEKIQ=true bundle exec unicorn 65 | ``` 66 | 67 | ### Heroku 68 | 69 | ```bash 70 | heroku create --addons heroku-postgresql:dev,rediscloud:20 71 | heroku pg:promote $(heroku pg | head -1 | cut -f2 -d" ") 72 | heroku config:add TENT_ENTITY=$(heroku info -s | grep web_url | cut -f2 -d"=" | sed 's/http/https/' | sed 's/\/$//') 73 | git push heroku master 74 | heroku run rake db:migrate 75 | ``` 76 | 77 | *Note: You will need to checkin `Gemfile.lock` after running `bundle install` to push to heroku* 78 | 79 | ## Testing 80 | 81 | ### ENV Variables 82 | 83 | name | required | description 84 | -------------------------------- | -------- | ----------- 85 | TEST_DATABASE_URL | Required | URL of postgres database. 86 | TEST_VALIDATOR_TEND_DATABASE_URL | Required | URL of postgres database. 87 | REDIS_URL | Optional | Defaults to `redis://localhost:6379/0`. A redis server is required. 88 | 89 | ### Running Tests 90 | 91 | ```bash 92 | bundle exec rake 93 | ``` 94 | 95 | ## Advanced 96 | 97 | ### Sidekiq Config 98 | 99 | ```ruby 100 | # sidekiq client (see `config.ru`) 101 | require 'tentd/worker' 102 | 103 | # pass redis options directly 104 | TentD::Worker.configure_client(:namespace => 'tentd.worker') 105 | 106 | # access sidekiq config directly 107 | TentD::Worker.configure_client do |sidekiq_config| 108 | # do stuff 109 | end 110 | ``` 111 | 112 | ```ruby 113 | # sidekiq server (see `sidekiq.rb`) 114 | require 'tentd/worker' 115 | 116 | # pass redis options directly 117 | TentD::Worker.configure_server(:namespace => 'tentd.worker') 118 | 119 | # access sidekiq config directly 120 | TentD::Worker.configure_server do |sidekiq_config| 121 | # do stuff 122 | end 123 | ``` 124 | 125 | ```ruby 126 | # run sidekiq server from current proccess 127 | require 'tentd/worker' 128 | 129 | sidekiq_pid = TentD::Worker.run_server(:namespace => 'tentd.worker') 130 | 131 | at_exit do 132 | Process.kill("INT", sidekiq_pid) 133 | end 134 | ``` 135 | 136 | *Note that blocks are called after calling `config.redis = options` (see `lib/tentd/worker.rb`)* 137 | 138 | ## Contributing 139 | 140 | - Refactor. The current code was hacked together quickly and is pretty ugly. 141 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'bundler/gem_tasks' 3 | 4 | task :validator_spec do 5 | $stdout, $stderr = STDOUT.dup, STDERR.dup 6 | 7 | # get random port 8 | require 'socket' 9 | tmp_socket = Socket.new(:INET, :STREAM) 10 | tmp_socket.bind(Addrinfo.tcp("127.0.0.1", 0)) 11 | host, port = tmp_socket.local_address.getnameinfo 12 | tmp_socket.close 13 | 14 | def puts_error(e) 15 | $stderr.print "#{e.inspect}:\n\t" 16 | $stderr.puts e.backtrace.slice(0, 20).join("\n\t") 17 | end 18 | 19 | tentd_pid = fork do 20 | require 'puma/cli' 21 | 22 | STDOUT.reopen '/dev/null' 23 | STDERR.reopen '/dev/null' 24 | 25 | # don't show database activity 26 | ENV['DB_LOGFILE'] ||= '/dev/null' 27 | 28 | ENV['TENT_ENTITY'] ||= "http://localhost:#{port}#{ENV['TENT_SUBDIR']}" 29 | 30 | # use test database 31 | unless ENV['DATABASE_URL'] = ENV['TEST_DATABASE_URL'] 32 | STDERR.puts "You must set TEST_DATABASE_URL!" 33 | exit 1 34 | end 35 | 36 | $stdout.puts "Booting Tent server on port #{port}..." 37 | 38 | ENV['RUN_SIDEKIQ'] = 'true' # Boot sidekiq server 39 | ENV['SIDEKIQ_LOG'] = File.join(File.expand_path(File.dirname(__FILE__)), 'sidekiq.log') 40 | 41 | rackup_path = File.expand_path(File.join(File.dirname(__FILE__), 'config.ru')) 42 | cli = Puma::CLI.new ['--port', port.to_s, rackup_path] 43 | begin 44 | cli.run 45 | rescue => e 46 | puts_error(e) 47 | exit 1 48 | end 49 | end 50 | 51 | validator_pid = fork do 52 | validator_pid = Process.pid 53 | at_exit do 54 | if Process.pid == validator_pid 55 | $stdout.puts "Stopping Tent server (PID: #{tentd_pid})..." 56 | begin 57 | Process.kill("INT", tentd_pid) 58 | rescue Errno::ESRCH 59 | end 60 | end 61 | end 62 | 63 | ENV['SIDEKIQ_LOG'] = File.join(File.expand_path(File.dirname(__FILE__)), 'validator-sidekiq.log') 64 | 65 | # always use postgres for attachments 66 | ENV['POSTGRES_ATTACHMENTS'] = 'true' 67 | 68 | # wait until tentd server boots 69 | tentd_started = false 70 | until tentd_started 71 | begin 72 | Socket.tcp("127.0.0.1", port) do |connection| 73 | tentd_started = true 74 | connection.close 75 | end 76 | rescue Errno::ECONNREFUSED 77 | rescue Interrupt 78 | exit 79 | end 80 | end 81 | 82 | # don't show database activity 83 | ENV['DB_LOGFILE'] ||= '/dev/null' 84 | 85 | begin 86 | require 'tent-validator' 87 | server_url = "http://localhost:#{port}#{ENV['TENT_SUBDIR']}" 88 | TentValidator.setup!( 89 | :remote_entity_uri => server_url, 90 | :tent_database_url => ENV['TEST_VALIDATOR_TEND_DATABASE_URL'] 91 | ) 92 | TentValidator::Runner::CLI.run 93 | rescue => e 94 | puts_error(e) 95 | exit 1 96 | end 97 | end 98 | 99 | # wait for tentd process to exit 100 | Process.waitpid(tentd_pid) 101 | 102 | if $?.exitstatus == 0 103 | Process.waitpid(validator_pid) 104 | else 105 | # kill validator if tentd exits first with non-0 status 106 | $stdout.puts "Stopping Validator (PID: #{validator_pid})..." 107 | begin 108 | Process.kill("INT", validator_pid) 109 | rescue Errno::ESRCH 110 | end 111 | end 112 | 113 | exit $?.exitstatus 114 | end 115 | 116 | task :default => :validator_spec 117 | 118 | lib = File.expand_path(File.join(File.dirname(__FILE__), 'lib')) 119 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 120 | require 'tentd/tasks/db' 121 | -------------------------------------------------------------------------------- /boot.rb: -------------------------------------------------------------------------------- 1 | lib = File.expand_path(File.join(File.dirname(__FILE__), 'lib')) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'tentd' 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | require 'bundler/setup' 5 | 6 | if !ENV['RUN_SIDEKIQ'].nil? 7 | # run sidekiq server 8 | require 'tentd/worker' 9 | sidekiq_pid = TentD::Worker.run_server 10 | 11 | puts "Sidekiq server running (pid: #{sidekiq_pid})" 12 | else 13 | sidekiq_pid = nil 14 | end 15 | 16 | require 'tentd' 17 | 18 | TentD.setup! 19 | 20 | TentD::Worker.configure_client 21 | 22 | map (ENV['TENT_SUBDIR'] || '') + '/' do 23 | run TentD::API.new 24 | end 25 | 26 | if sidekiq_pid 27 | at_exit do 28 | puts "Killing sidekiq server (pid: #{sidekiq_pid})..." 29 | Process.kill("INT", sidekiq_pid) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrations/001_initial_schema.rb: -------------------------------------------------------------------------------- 1 | Sequel.migration do 2 | change do 3 | 4 | # Contents: 5 | # - entities 6 | # - types 7 | # - users 8 | # - posts 9 | # - apps 10 | # - relationships 11 | # - subscriptions 12 | # - groups 13 | # - mentions 14 | # - refs 15 | # - attachments 16 | # - posts_attachments 17 | # - permissions 18 | 19 | create_table(:entities) do 20 | primary_key :id 21 | 22 | column :entity , "text" , :null => false 23 | 24 | index [:entity], :name => :unique_entities, :unique => true 25 | end 26 | 27 | create_table(:types) do 28 | primary_key :id 29 | 30 | column :base , "text" , :null => false 31 | column :version , "text" , :null => false 32 | column :fragment , "text" 33 | 34 | index [:base, :version, :fragment], :name => :unique_types, :unique => true 35 | end 36 | 37 | create_table(:users) do 38 | primary_key :id 39 | foreign_key :entity_id, :entities 40 | 41 | column :entity , "text" , :null => false # entities.entity 42 | column :meta_post_id , "bigint" # posts.id 43 | 44 | column :server_credentials , "text" , :null => false 45 | column :deleted_at , "timestamp without time zone" 46 | 47 | index [:entity_id], :name => :unique_users, :unique => true 48 | end 49 | 50 | create_table(:posts) do 51 | primary_key :id 52 | foreign_key :user_id , :users 53 | foreign_key :type_id , :types 54 | foreign_key :type_base_id , :types 55 | foreign_key :entity_id , :entities 56 | 57 | column :type , "text" , :null => false # types.type + '#' + types.fragment 58 | column :entity , "text" , :null => false # entities.entity 59 | column :original_entity , "text" 60 | 61 | # Timestamps 62 | # milliseconds since unix epoch 63 | # bigint max value: 9,223,372,036,854,775,807 64 | 65 | column :published_at , "bigint" , :null => false 66 | column :received_at , "bigint" 67 | column :version_published_at , "bigint" 68 | column :version_received_at , "bigint" 69 | column :deleted_at , "timestamp without time zone" 70 | 71 | column :app_id , "text" 72 | column :app_name , "text" 73 | column :app_url , "text" 74 | 75 | column :public , "boolean" , :default => false 76 | column :permissions_entities , "text[]" , :default => "{}" 77 | column :permissions_groups , "text[]" , :default => "{}" 78 | 79 | column :mentions , "text" # serialized json 80 | column :refs , "text" # serialized json 81 | column :attachments , "text" # serialized json 82 | 83 | column :version_parents , "text" # serialized json 84 | column :version , "text" , :null => false 85 | column :version_message , "text" 86 | 87 | column :public_id , "text" 88 | column :licenses , "text" # serialized json 89 | column :content , "text" # serialized json 90 | 91 | index [:user_id], :name => :index_posts_user 92 | index [:user_id, :public_id], :name => :index_posts_user_public_id 93 | index [:user_id, :entity_id, :public_id, :version], :name => :unique_posts, :unique => true 94 | end 95 | 96 | create_table(:parents) do 97 | foreign_key :post_id 98 | foreign_key :parent_post_id 99 | 100 | column :version, "text" 101 | column :post, "text" 102 | 103 | index [:post_id, :version, :post], :name => :unique_post_parents, :unique => true 104 | end 105 | 106 | create_table(:delivery_failures) do 107 | primary_key :id 108 | foreign_key :user_id , :users 109 | foreign_key :failed_post_id , :posts # post for which delivery failed 110 | foreign_key :post_id , :posts # delivery failure post 111 | 112 | column :entity , "text" , :null => false # entity to whom delivery failed 113 | column :status , "text" , :null => false 114 | column :reason , "text" , :null => false 115 | 116 | index [:user_id, :failed_post_id, :entity, :status], :name => :unique_delivery_failrues, :unique => true 117 | end 118 | 119 | create_table(:apps) do 120 | primary_key :id 121 | foreign_key :user_id , :users 122 | foreign_key :post_id , :posts 123 | foreign_key :auth_post_id , :posts 124 | foreign_key :credentials_post_id , :posts 125 | foreign_key :auth_credentials_post_id , :posts 126 | 127 | column :hawk_key , "text" # credentials_post.content.hawk_key 128 | column :auth_hawk_key , "text" # auth_credentials_post.content.hawk_key 129 | 130 | column :notification_url , "text" # post.content.notification_url 131 | column :notification_type_ids , "text[]" 132 | 133 | column :read_types , "text[]" # auth_post.content.types.read 134 | column :read_type_ids , "text[]" # auth_post.content.types.read ids 135 | column :write_types , "text[]" # auth_post.content.types.write 136 | column :scopes , "text[]" # auth_post.content.scopes 137 | column :deleted_at , "timestamp without time zone" 138 | 139 | index [:user_id, :auth_hawk_key], :name => :index_apps_user_auth_hawk_key 140 | index [:user_id, :post_id], :name => :unique_app, :unique => true 141 | end 142 | 143 | create_table(:relationships) do 144 | primary_key :id 145 | foreign_key :user_id , :users 146 | foreign_key :entity_id , :entities 147 | foreign_key :post_id , :posts 148 | foreign_key :meta_post_id , :posts 149 | foreign_key :credentials_post_id , :posts 150 | foreign_key :type_id , :types # type of relationship, posts.type where posts.id = post_id 151 | 152 | column :active , "boolean" 153 | column :entity , "text" 154 | column :remote_credentials_id , "text" # remote_credentials.id (public_id) 155 | column :remote_credentials , "text" # serialized as json (used to sign outgoing requests) 156 | column :deleted_at , "timestamp without time zone" 157 | 158 | index [:user_id, :type_id], :name => :index_relationships_user_type 159 | index [:user_id, :entity_id], :name => :unique_relationships, :unique => true 160 | end 161 | 162 | create_table(:subscriptions) do 163 | primary_key :id 164 | foreign_key :user_id , :users 165 | foreign_key :post_id , :posts 166 | foreign_key :subscriber_entity_id , :entities # entity of subscriber 167 | foreign_key :entity_id , :entities # entity subscribed to 168 | foreign_key :type_id , :types # null if type = 'all' 169 | 170 | column :subscriber_entity , "text" # entity of subscriber 171 | column :entity , "text" # entity subscribed to 172 | column :type , "text" # type uri or 'all' 173 | column :deleted_at , "timestamp without time zone" 174 | 175 | index [:user_id, :type_id], :name => :index_subscriptions_user_type 176 | index [:user_id, :entity_id, :subscriber_entity_id, :type_id], :name => :unique_subscriptions, :unique => true 177 | end 178 | 179 | create_table(:groups) do 180 | primary_key :id 181 | foreign_key :user_id , :users 182 | foreign_key :post_id , :posts 183 | 184 | index [:user_id, :post_id], :name => :unique_groups, :unique => true 185 | end 186 | 187 | create_table(:mentions) do 188 | foreign_key :user_id , :users 189 | foreign_key :post_id , :posts 190 | foreign_key :entity_id , :entities 191 | foreign_key :type_id , :types 192 | 193 | column :type , "text" 194 | column :entity , "text" 195 | column :post , "text" 196 | column :public , "boolean" , :default => true 197 | 198 | index [:user_id, :post_id, :entity_id, :post], :name => :unique_mentions, :unique => true 199 | end 200 | 201 | create_table(:refs) do 202 | foreign_key :user_id , :users 203 | foreign_key :post_id , :posts 204 | foreign_key :entity_id , :entities 205 | 206 | column :post , "text" 207 | 208 | index [:user_id, :post_id, :entity_id, :post], :name => :unique_refs, :unique => true 209 | end 210 | 211 | # Fallback data store for attachments 212 | # metadata is kept in posts.attachment as serialized json 213 | # 214 | # No need to scope by user as attachment data is immutable 215 | create_table(:attachments) do 216 | primary_key :id 217 | 218 | column :digest , "text" , :null => false 219 | column :size , "bigint" , :null => false 220 | column :data_oid , "bigint" , :null => false 221 | 222 | index [:digest, :size], :name => :unique_attachments, :unique => true 223 | end 224 | 225 | # Join table for post attachment data 226 | create_table(:posts_attachments) do 227 | foreign_key :post_id , :posts , :on_delete => :cascade 228 | foreign_key :attachment_id , :attachments , :on_delete => :cascade 229 | 230 | column :digest , "text" 231 | column :content_type , "text" , :null => false 232 | 233 | index [:post_id, :attachment_id, :content_type], :name => :unique_posts_attachments, :unique => true 234 | end 235 | 236 | create_table(:permissions) do 237 | foreign_key :user_id , :users 238 | foreign_key :post_id , :posts 239 | foreign_key :entity_id , :entities 240 | foreign_key :group_id , :groups 241 | 242 | index [:user_id, :post_id, :entity_id, :group_id], :name => :unique_permissions, :unique => true 243 | end 244 | 245 | end 246 | end 247 | -------------------------------------------------------------------------------- /lib/tentd.rb: -------------------------------------------------------------------------------- 1 | require 'tentd/version' 2 | require 'tentd/utils' 3 | require 'tent-client' 4 | 5 | module TentD 6 | 7 | TENT_VERSION = '0.3'.freeze 8 | 9 | TentType = TentClient::TentType 10 | 11 | module REGEX 12 | VALID_ID = /\A[-0-9a-z_]+\Z/i 13 | end 14 | 15 | def self.settings 16 | @settings ||= { 17 | :debug => ENV['DEBUG'] == 'true' 18 | } 19 | end 20 | 21 | def self.logger 22 | return self.settings[:logger] if self.settings[:logger] 23 | 24 | require 'logger' 25 | self.settings[:logger] = Logger.new(STDOUT, STDERR) 26 | 27 | self.settings[:logger] 28 | end 29 | 30 | def self.setup!(options = {}) 31 | setup_database!(options) 32 | 33 | require 'tentd/worker' 34 | require 'tentd/query' 35 | require 'tentd/feed' 36 | require 'tentd/refs' 37 | require 'tentd/proxied_post' 38 | require 'tentd/authorizer' 39 | require 'tentd/request_proxy_manager' 40 | require 'tentd/relationship_importer' 41 | require 'tentd/api' 42 | end 43 | 44 | def self.setup_database!(options = {}) 45 | require 'sequel' 46 | require 'logger' 47 | 48 | if database_url = options[:database_url] || ENV['DATABASE_URL'] 49 | @database = Sequel.connect(database_url, :logger => options[:database_logger] || Logger.new(ENV['DB_LOGFILE'] || STDOUT)) 50 | end 51 | 52 | require 'tentd/query' 53 | require 'tentd/model' 54 | 55 | Model.soft_delete = ENV['SOFT_DELETE'].to_s != 'false' 56 | 57 | if (aws_access_key_id = options[:aws_access_key_id] || ENV['AWS_ACCESS_KEY_ID']) && 58 | (aws_secret_access_key = options[:aws_secret_access_key] || ENV['AWS_SECRET_ACCESS_KEY']) 59 | # use S3 for attachments 60 | 61 | 62 | fog_adapter = { 63 | :provider => 'AWS', 64 | :aws_access_key_id => aws_access_key_id, 65 | :aws_secret_access_key => aws_secret_access_key 66 | } 67 | 68 | if aws_host = options[:aws_host] || ENV['AWS_HOST'] 69 | fog_adapter[:host] = aws_host 70 | end 71 | 72 | if aws_port = options[:aws_port] || ENV['AWS_PORT'] 73 | fog_adapter[:port] = aws_port 74 | end 75 | 76 | if aws_scheme = options[:aws_scheme] || ENV['AWS_SCHEME'] 77 | fog_adapter[:scheme] = aws_scheme 78 | end 79 | 80 | elsif (google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID']) && 81 | (google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY']) 82 | 83 | fog_adapter = { 84 | :provider => 'Google', 85 | :google_storage_secret_access_key_id => google_storage_secret_access_key_id, 86 | :google_storage_secret_access_key => google_storage_secret_access_key 87 | } 88 | elsif (rackspace_username = ENV['RACKSPACE_USERNAME']) && 89 | (rackspace_api_key = ENV['RACKSPACE_API_KEY']) 90 | 91 | fog_adapter = { 92 | :provider => 'Rackspace', 93 | :rackspace_username => rackspace_username, 94 | :rackspace_api_key => rackspace_api_key 95 | } 96 | 97 | if rackspace_auth_url = ENV['RACKSPACE_AUTH_URL'] 98 | fog_adapter[:rackspace_auth_url] = rackspace_auth_url 99 | end 100 | elsif path = ENV['LOCAL_ATTACHMENTS_ROOT'] 101 | fog_adapter = { 102 | :provider => 'Local', 103 | :local_root => path 104 | } 105 | else 106 | fog_adapter = nil 107 | end 108 | 109 | # force use of postgres for attachments 110 | if ENV['POSTGRES_ATTACHMENTS'].to_s == 'true' 111 | fog_adapter = nil 112 | end 113 | 114 | if fog_adapter 115 | require 'tentd/models/attachment/fog' 116 | 117 | Model::Attachment.fog_adapter = fog_adapter 118 | 119 | Model::Attachment.namespace = options[:attachments_namespace] || ENV['S3_BUCKET'] || ENV['ATTACHMENTS_NAMESPACE'] || 'tentd-attachments' 120 | else 121 | require 'tentd/models/attachment/sequel' 122 | end 123 | end 124 | 125 | def self.database 126 | @database 127 | end 128 | 129 | end 130 | -------------------------------------------------------------------------------- /lib/tentd/api.rb: -------------------------------------------------------------------------------- 1 | require 'rack-putty' 2 | 3 | module TentD 4 | class API 5 | 6 | CREDENTIALS_LINK_REL = %(https://tent.io/rels/credentials).freeze 7 | 8 | POST_CONTENT_MIME = %(application/vnd.tent.post.v0+json).freeze 9 | MULTIPART_CONTENT_MIME = %(multipart/form-data).freeze 10 | 11 | CREDENTIALS_MIME_TYPE = %(https://tent.io/types/credentials/v0).freeze 12 | RELATIONSHIP_MIME_TYPE = %(https://tent.io/types/relationship/v0#).freeze 13 | 14 | POST_CONTENT_TYPE = %(#{POST_CONTENT_MIME}; type="%s").freeze 15 | ERROR_CONTENT_TYPE = %(application/vnd.tent.error.v0+json).freeze 16 | MULTIPART_CONTENT_TYPE = MULTIPART_CONTENT_MIME 17 | 18 | MENTIONS_CONTENT_TYPE = %(application/vnd.tent.post-mentions.v0+json).freeze 19 | CHILDREN_CONTENT_TYPE = %(application/vnd.tent.post-children.v0+json).freeze 20 | VERSIONS_CONTENT_TYPE = %(application/vnd.tent.post-versions.v0+json).freeze 21 | 22 | PROXY_HEADERS = Set.new(%w( 23 | Content-Type 24 | Content-Length 25 | ETag 26 | )).freeze 27 | 28 | require 'tentd/api/middleware' 29 | require 'tentd/api/middleware/hello_world' 30 | require 'tentd/api/middleware/discover' 31 | require 'tentd/api/middleware/not_found' 32 | require 'tentd/api/middleware/user_lookup' 33 | require 'tentd/api/middleware/authentication' 34 | require 'tentd/api/middleware/authorization' 35 | require 'tentd/api/middleware/parse_input_data' 36 | require 'tentd/api/middleware/parse_content_type' 37 | require 'tentd/api/middleware/parse_link_header' 38 | require 'tentd/api/middleware/validate_input_data' 39 | require 'tentd/api/middleware/validate_post_content_type' 40 | require 'tentd/api/middleware/set_request_proxy_manager' 41 | require 'tentd/api/middleware/proxy_post_list' 42 | require 'tentd/api/middleware/list_post_mentions' 43 | require 'tentd/api/middleware/list_post_children' 44 | require 'tentd/api/middleware/list_post_versions' 45 | require 'tentd/api/middleware/authorize_get_entity' 46 | require 'tentd/api/middleware/lookup_post' 47 | require 'tentd/api/middleware/get_post' 48 | require 'tentd/api/middleware/serve_post' 49 | require 'tentd/api/middleware/proxy_attachment_redirect' 50 | require 'tentd/api/middleware/attachment_redirect' 51 | require 'tentd/api/middleware/get_attachment' 52 | require 'tentd/api/middleware/create_post' 53 | require 'tentd/api/middleware/create_post_version' 54 | require 'tentd/api/middleware/delete_post' 55 | require 'tentd/api/middleware/posts_feed' 56 | 57 | require 'tentd/api/serialize_response' 58 | require 'tentd/api/cors_headers' 59 | require 'tentd/api/relationship_initialization' 60 | require 'tentd/api/notification_importer' 61 | require 'tentd/api/oauth' 62 | require 'tentd/api/meta_profile' 63 | 64 | include Rack::Putty::Router 65 | 66 | stack_base SerializeResponse 67 | 68 | middleware CorsHeaders 69 | middleware UserLookup 70 | middleware Authentication 71 | middleware Authorization 72 | middleware ParseInputData 73 | middleware ParseContentType 74 | middleware ParseLinkHeader 75 | middleware SetRequestProxyManager 76 | 77 | match '/' do |b| 78 | b.use HelloWorld 79 | end 80 | 81 | options %r{/.*} do |b| 82 | b.use CorsPreflight 83 | end 84 | 85 | post '/posts' do |b| 86 | b.use ValidateInputData 87 | b.use ValidatePostContentType 88 | b.use CreatePost 89 | b.use ServePost 90 | end 91 | 92 | get '/posts/:entity/:post' do |b| 93 | b.use AuthorizeGetEntity 94 | b.use ProxyPostList 95 | b.use LookupPost 96 | b.use ProxyPostList # lookup failed, proxy_condition is :on_miss 97 | b.use GetPost 98 | b.use ServePost 99 | end 100 | 101 | put '/posts/:entity/:post' do |b| 102 | b.use ValidateInputData 103 | b.use CreatePostVersion 104 | b.use ServePost 105 | end 106 | 107 | delete '/posts/:entity/:post' do |b| 108 | b.use LookupPost 109 | b.use DeletePost 110 | b.use ServePost 111 | end 112 | 113 | get '/posts/:entity/:post/attachments/:name' do |b| 114 | b.use ProxyAttachmentRedirect 115 | b.use LookupPost 116 | b.use ProxyAttachmentRedirect # lookup failed, proxy_condition is :on_miss 117 | b.use GetPost 118 | b.use AttachmentRedirect 119 | end 120 | 121 | get '/attachments/:entity/:digest' do |b| 122 | b.use GetAttachment 123 | end 124 | 125 | get '/posts' do |b| 126 | b.use PostsFeed 127 | end 128 | 129 | get '/oauth/authorize' do |b| 130 | b.use OAuth::Authorize 131 | end 132 | 133 | get '/discover' do |b| 134 | b.use Discover 135 | end 136 | 137 | post '/oauth/token' do |b| 138 | b.use OAuth::Token 139 | end 140 | 141 | match %r{/.*} do |b| 142 | b.use NotFound 143 | end 144 | 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/tentd/api/cors_headers.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class CorsHeaders 5 | HEADERS = { 6 | 'Access-Control-Allow-Origin' => '*', 7 | 'Access-Control-Expose-Headers' => 'Content-Type, Count, ETag, Link, Server-Authorization, WWW-Authenticate', 8 | 'Access-Control-Max-Age' => '2592000' # 30 days 9 | }.freeze 10 | 11 | def initialize(app) 12 | @app = app 13 | end 14 | 15 | def call(env) 16 | status, headers, body = @app.call(env) 17 | 18 | headers.merge!(HEADERS.dup) if env['HTTP_ORIGIN'] 19 | 20 | [status, headers, body] 21 | end 22 | end 23 | 24 | class CorsPreflight 25 | HEADERS = { 26 | 'Access-Control-Allow-Origin' => '*', 27 | 'Access-Control-Allow-Methods' => 'DELETE, GET, HEAD, PATCH, POST, PUT', 28 | 'Access-Control-Allow-Headers' => 'Accept, Authorization, Cache-Control, Content-Type, If-Match, If-None-Match, Link', 29 | 'Access-Control-Expose-Headers' => 'Count, Link, Server-Authorization, Content-Type', 30 | 'Access-Control-Max-Age' => '2592000' # 30 days 31 | }.freeze 32 | 33 | def initialize(app) 34 | @app = app 35 | end 36 | 37 | def call(env) 38 | [200, HEADERS.dup, []] 39 | end 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/tentd/api/meta_profile.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class MetaProfile 5 | 6 | SPECIFIERS = %w( entity mentions refs parents permissions ).freeze 7 | 8 | def self.meta_type_id 9 | @meta_type_id ||= Model::Type.find_or_create_full("https://tent.io/types/meta/v0#").id 10 | end 11 | 12 | def self.profile_as_json(post) 13 | return unless Hash === post.content['profile'] 14 | data = Utils::Hash.deep_dup(post.content['profile']) 15 | data['avatar_digest'] = post.attachments.first['digest'] if post.attachments.to_a.any? 16 | data 17 | end 18 | 19 | attr_reader :env, :posts 20 | def initialize(env, posts) 21 | # posts is a mixture of Model::Post and TentD::ProxiedPost 22 | @env, @posts = env, posts 23 | end 24 | 25 | def profiles(specifiers) 26 | _entities = entities(specifiers) 27 | _entity_ids = entity_ids(_entities) 28 | 29 | return {} unless _entities.any? 30 | 31 | unless request_proxy_manager.proxy_condition == :always 32 | models = Model::Post.where( 33 | :user_id => current_user_id, 34 | :type_id => meta_type_id, 35 | :entity_id => _entity_ids 36 | ).order(:public_id, Sequel.desc(:version_received_at)).distinct(:public_id).all.to_a 37 | 38 | _entities -= models.map(&:entity) 39 | else 40 | models = [] 41 | end 42 | 43 | unless request_proxy_manager.proxy_condition == :never 44 | _meta_profiles = _entities.inject({}) do |memo, entity| 45 | fetch_meta_profile(entity) do |meta_profile| 46 | memo[entity] = meta_profile 47 | end 48 | 49 | memo 50 | end 51 | else 52 | _meta_profiles = {} 53 | end 54 | 55 | models.inject(_meta_profiles) { |memo, post| 56 | memo[post.entity] = profile_as_json(post) 57 | memo 58 | } 59 | end 60 | 61 | def profile_as_json(post) 62 | self.class.profile_as_json(post) 63 | end 64 | 65 | private 66 | 67 | def authorizer 68 | @authorizer ||= Authorizer.new(env) 69 | end 70 | 71 | def fetch_meta_profile(entity, &block) 72 | if entity == current_user.entity 73 | return current_user.meta_post.as_json 74 | end 75 | 76 | return unless meta_post = TentClient.new(entity).server_meta_post 77 | return unless meta_profile = meta_post['content']['profile'] 78 | 79 | if meta_post['attachments'].to_a.any? 80 | meta_profile['avatar_digest'] = meta_post['attachments'][0]['digest'] 81 | end 82 | 83 | yield meta_profile 84 | end 85 | 86 | def current_user 87 | @current_user ||= env['current_user'] 88 | end 89 | 90 | def current_user_id 91 | @current_user_id ||= current_user.id 92 | end 93 | 94 | def request_proxy_manager 95 | @request_proxy_manager ||= env['request_proxy_manager'] 96 | end 97 | 98 | def meta_type_id 99 | self.class.meta_type_id 100 | end 101 | 102 | def entity_ids(_entities) 103 | return [] unless _entities.any? 104 | 105 | _entity_id_mapping = {} 106 | posts.each { |post| _entity_id_mapping[post.entity] = post.entity_id } 107 | 108 | _entity_ids = [] 109 | _entities.each { |entity| 110 | if _id = _entity_id_mapping[entity] 111 | _entity_ids.push(_id) 112 | end 113 | } 114 | _entities -= _entity_id_mapping.keys 115 | return _entity_ids unless _entities.any? 116 | 117 | _entity_ids + Model::Entity.select(:id).where(:entity => _entities).all.to_a.map(&:id) 118 | end 119 | 120 | def entities(specifiers) 121 | return [] unless posts.any? 122 | 123 | specifiers = specifiers & SPECIFIERS 124 | return [] unless specifiers.any? 125 | 126 | posts.inject([]) do |memo, post| 127 | next memo unless authorizer.read_entity?(post.entity) 128 | 129 | specifiers.each do |specifier| 130 | case specifier 131 | when 'entity' 132 | memo << post.entity 133 | when 'mentions' 134 | memo += post.mentions.to_a.map { |m| m['entity'] || post.entity } 135 | when 'refs' 136 | memo += post.refs.to_a.map { |r| r['entity'] || post.entity } 137 | when 'parents' 138 | memo += post.version_parents.to_a.map { |p| p['entity'] || post.entity } 139 | end 140 | end.uniq 141 | 142 | memo 143 | end 144 | end 145 | end 146 | 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware.rb: -------------------------------------------------------------------------------- 1 | require 'yajl' 2 | module TentD 3 | class API 4 | 5 | class Middleware < Rack::Putty::Middleware 6 | 7 | class Halt < StandardError 8 | attr_accessor :code, :message, :attributes, :headers 9 | def initialize(code, message, attributes = {}) 10 | super(message) 11 | @code, @message, @attributes = code, message, attributes 12 | @headers = attributes.delete(:headers) || {} 13 | end 14 | end 15 | 16 | def call(env) 17 | super 18 | rescue Halt => e 19 | [e.code, { 'Content-Type' => ERROR_CONTENT_TYPE }.merge(e.headers), [encode_json(e.attributes.merge(:error => e.message))]] 20 | end 21 | 22 | def encode_json(data) 23 | Yajl::Encoder.encode(data) 24 | end 25 | 26 | def halt!(status, message, attributes = {}) 27 | raise Halt.new(status, message, attributes) 28 | end 29 | 30 | def rack_input(env) 31 | data = env['rack.input'].read 32 | env['rack.input'].rewind 33 | data 34 | end 35 | 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/attachment_redirect.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class AttachmentRedirect < Middleware 5 | def action(env) 6 | unless Model::Post === env['response.post'] 7 | env.delete('response.post') 8 | return env 9 | end 10 | 11 | post = env.delete('response.post') 12 | params = env['params'] 13 | 14 | accept = env['HTTP_ACCEPT'].to_s.split(';').first 15 | 16 | attachments = post.attachments.select { |a| a['name'] == params[:name] } 17 | 18 | unless accept == '*/*' 19 | attachments = attachments.select { |a| a['content_type'] == accept } 20 | end 21 | return env if attachments.empty? 22 | 23 | attachment = attachments.first 24 | 25 | attachment_url = Utils.expand_uri_template(env['current_user'].preferred_server['urls']['attachment'], 26 | :entity => post.entity, 27 | :digest => attachment['digest'] 28 | ) 29 | 30 | (env['response.headers'] ||= {})['Location'] = attachment_url 31 | env['response.headers']['Attachment-Digest'] = attachment['digest'] 32 | env['response.status'] = 302 33 | 34 | env 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/authentication.rb: -------------------------------------------------------------------------------- 1 | require 'hawk' 2 | 3 | module TentD 4 | class API 5 | 6 | class Authentication < Middleware 7 | 8 | def action(env) 9 | header_authentication!(env) 10 | bewit_authentication!(env) 11 | env 12 | end 13 | 14 | def header_authentication!(env) 15 | return unless auth_header = env['HTTP_AUTHORIZATION'] 16 | 17 | res = Hawk::Server.authenticate( 18 | auth_header, 19 | :credentials_lookup => proc { |id| lookup_credentials(env, id) }, 20 | :nonce_lookup => proc { |nonce| lookup_nonce(env, nonce) }, 21 | :content_type => simple_content_type(env), 22 | :payload => rack_input(env), 23 | :host => request_host(env), 24 | :request_uri => env['REQUEST_URI'], 25 | :port => request_port(env), 26 | :method => request_method(env) 27 | ) 28 | 29 | if Hawk::AuthenticationFailure === res 30 | if res.key == :ts 31 | halt!(401, "Authentication failure: #{res.message}", :headers => { 32 | "WWW-Authenticate" => res.header 33 | }) 34 | else 35 | halt!(403, "Authentication failure: #{res.message}") 36 | end 37 | else 38 | env['current_auth'] = res 39 | end 40 | end 41 | 42 | def bewit_authentication!(env) 43 | return unless bewit = env['params']['bewit'] 44 | 45 | res = Hawk::Server.authenticate_bewit( 46 | bewit, 47 | :credentials_lookup => proc { |id| lookup_credentials(env, id) }, 48 | :host => request_host(env), 49 | :request_uri => env['REQUEST_URI'], 50 | :port => request_port(env), 51 | :method => request_method(env) 52 | ) 53 | 54 | if Hawk::AuthenticationFailure === res 55 | halt!(403, "Authentication failure: #{res.message}") 56 | else 57 | env['current_auth'] = res 58 | end 59 | end 60 | 61 | private 62 | 63 | def lookup_credentials(env, id) 64 | return unless id =~ TentD::REGEX::VALID_ID 65 | 66 | return unless credentials = if id == env['current_user'].server_credentials['id'] 67 | resource = env['current_user'] 68 | TentD::Utils::Hash.symbolize_keys(env['current_user'].server_credentials) 69 | elsif credentials_post = Model::Credentials.lookup(env['current_user'], id) 70 | resource = credentials_post 71 | Model::Credentials.slice_credentials(credentials_post) 72 | end 73 | 74 | return unless credentials 75 | 76 | { 77 | :id => credentials[:id], 78 | :key => credentials[:hawk_key], 79 | :algorithm => credentials[:hawk_algorithm], 80 | :credentials_resource => resource 81 | } 82 | end 83 | 84 | def lookup_nonce(env, nonce) 85 | end 86 | 87 | def simple_content_type(env) 88 | env['CONTENT_TYPE'].to_s.split(';').first 89 | end 90 | 91 | def request_host(env) 92 | env['SERVER_NAME'] 93 | end 94 | 95 | def request_path(env) 96 | env['PATH_INFO'] + (env['QUERY_STRING'] != "" ? "?#{env['QUERY_STRING']}" : "") 97 | end 98 | 99 | def request_port(env) 100 | ENV['SERVER_PORT'] || env['SERVER_PORT'] 101 | end 102 | 103 | def request_method(env) 104 | env['REQUEST_METHOD'] 105 | end 106 | 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/authorization.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class Authorization < Middleware 5 | def action(env) 6 | return env unless env['current_auth'] 7 | 8 | if Model::Post === env['current_auth'][:credentials_resource] 9 | type_bases = %w( 10 | https://tent.io/types/app-auth 11 | https://tent.io/types/app 12 | https://tent.io/types/relationship 13 | ) 14 | 15 | credentials_post = env['current_auth'][:credentials_resource] 16 | mention = credentials_post.mentions.to_a.find do |mention| 17 | type_bases.include?(TentType.new(mention['type']).base) 18 | end 19 | return env unless mention 20 | 21 | resource = Model::Post.where( 22 | :user_id => env['current_user'].id, 23 | :public_id => mention['post'] 24 | ).order(Sequel.desc(:version_received_at)).first 25 | return env unless resource 26 | 27 | env['current_auth.resource'] = resource 28 | else 29 | return env 30 | end 31 | 32 | env 33 | end 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/authorize_get_entity.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class AuthorizeGetEntity < Middleware 5 | def action(env) 6 | entity = env['params'][:entity] 7 | unless entity == env['current_user'].entity 8 | auth_candidate = Authorizer.new(env).auth_candidate 9 | halt!(404, "Not Found") unless auth_candidate && auth_candidate.read_entity?(entity) 10 | end 11 | 12 | env 13 | end 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/create_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class CreatePost < Middleware 5 | def action(env) 6 | unless write_authorized?(env) 7 | halt!(403, "Unauthorized") 8 | end 9 | 10 | begin 11 | if TentType.new(env['data']['type']).base == %(https://tent.io/types/app-auth) 12 | post = Model::AppAuth.create_from_env(env) 13 | else 14 | post = Model::Post.create_from_env(env) 15 | end 16 | rescue Model::Post::CreateFailure => e 17 | halt!(400, e.message) 18 | end 19 | 20 | env['response.post'] = post 21 | 22 | if %w( https://tent.io/types/app https://tent.io/types/app-auth ).include?(TentType.new(post.type).base) 23 | if TentType.new(post.type).base == "https://tent.io/types/app" 24 | # app 25 | credentials_post = Model::Post.first(:id => Model::App.first(:user_id => env['current_user'].id, :post_id => post.id).credentials_post_id) 26 | else 27 | # app-auth 28 | credentials_post = Model::Post.qualify.join(:mentions, :posts__id => :mentions__post_id).where( 29 | :mentions__post => post.public_id, 30 | :posts__type_base_id => Model::Type.find_or_create_base('https://tent.io/types/credentials/v0#').id 31 | ).first 32 | end 33 | 34 | current_user = env['current_user'] 35 | (env['response.links'] ||= []) << { 36 | :url => TentD::Utils.sign_url( 37 | current_user.server_credentials, 38 | TentD::Utils.expand_uri_template( 39 | current_user.preferred_server['urls']['post'], 40 | :entity => current_user.entity, 41 | :post => credentials_post.public_id 42 | ) 43 | ), 44 | :rel => "https://tent.io/rels/credentials" 45 | } 46 | end 47 | 48 | env 49 | end 50 | 51 | private 52 | 53 | def write_authorized?(env) 54 | writeable_types = %w[ 55 | https://tent.io/types/app 56 | https://tent.io/types/relationship 57 | ] 58 | 59 | authorizer = Authorizer.new(env) 60 | 61 | authorizer.write_authorized?(env['data']['entity'], env['data']['type']) || 62 | (!authorizer.auth_candidate && writeable_types.include?(TentType.new(env['data']['type']).base)) 63 | end 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/create_post_version.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class CreatePostVersion < Middleware 5 | def action(env) 6 | TentD.logger.debug "CreatePostVersion#action" if TentD.settings[:debug] 7 | 8 | if env['request.notification'] 9 | TentD.logger.debug "CreatePostVersion: notification request" if TentD.settings[:debug] 10 | 11 | case env['request.type'].to_s 12 | when "https://tent.io/types/relationship/v0#initial" 13 | TentD.logger.debug "CreatePostVersion -> RelationshipInitialization.call" if TentD.settings[:debug] 14 | 15 | RelationshipInitialization.call(env) 16 | else 17 | TentD.logger.debug "CreatePostVersion -> NotificationImporter.call" if TentD.settings[:debug] 18 | 19 | NotificationImporter.call(env) 20 | end 21 | else 22 | TentD.logger.debug "CreatePostVersion: create post version" if TentD.settings[:debug] 23 | 24 | authorizer = Authorizer.new(env) 25 | entity, id, type = env['params']['entity'], env['params']['post'], env['data']['type'] 26 | unless authorizer.write_post_id?(entity, id, type) 27 | TentD.logger.debug "CreatePostVersion: Unauthorized for write_post_id?(#{entity.inspect}, #{id.inspect}, #{type.inspect})" if TentD.settings[:debug] 28 | 29 | if env['current_auth'] 30 | halt!(403, "Unauthorized") 31 | else 32 | halt!(401, "Unauthorized") 33 | end 34 | end 35 | 36 | create_options = {} 37 | create_options[:import] = true if env['request.import'] 38 | 39 | begin 40 | env['response.post'] = Model::Post.create_version_from_env(env, create_options) 41 | rescue Model::Post::CreateFailure => e 42 | TentD.logger.debug "CreatePostVersion: CreateFailure: #{e.inspect}" if TentD.settings[:debug] 43 | 44 | halt!(400, e.message) 45 | end 46 | end 47 | 48 | env 49 | end 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/delete_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class DeletePost < Middleware 5 | def action(env) 6 | return env unless post = env.delete('response.post') 7 | params = env['params'] 8 | 9 | authorizer = Authorizer.new(env) 10 | unless authorizer.write_post?(post) 11 | if authorizer.read_authorized?(post) 12 | if authorizer.auth_candidate 13 | halt!(403, "Unauthorized") 14 | else 15 | halt!(401, "Unauthorized") 16 | end 17 | else 18 | halt!(404, "Not Found") 19 | end 20 | end 21 | 22 | delete_options = {} 23 | 24 | if env['HTTP_CREATE_DELETE_POST'] != "false" && post.entity_id == env['current_user'].entity_id 25 | delete_options[:create_delete_post] = true 26 | end 27 | 28 | if params[:version] 29 | delete_options[:delete_version] = params[:version] 30 | end 31 | 32 | post.user = env['current_user'] if post.user_id == env['current_user'].id # spare db lookup 33 | 34 | if delete_options[:create_delete_post] 35 | if delete_post = post.destroy(delete_options) 36 | env['response.post'] = delete_post 37 | else 38 | halt!(500, "Internal Server Error") 39 | end 40 | else 41 | if post.destroy(delete_options) 42 | env['response.status'] = 200 43 | else 44 | halt!(500, "Internal Server Error") 45 | end 46 | end 47 | 48 | env 49 | end 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/discover.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | class Discover < Middleware 4 | def action(env) 5 | unless Authorizer.new(env).proxy_authorized? 6 | halt!(403, "Unauthorized") 7 | end 8 | 9 | params = env['params'] 10 | 11 | unless params[:entity] && params[:entity].match(URI.regexp) 12 | halt!(400, "Entity param must be a valid url: #{Yajl::Encoder.encode(params[:entity])}") 13 | end 14 | 15 | status, headers, body = env['request_proxy_manager'].request(params[:entity]) do |client| 16 | TentClient::Discovery.new(client, params[:entity], :skip_serialization => true).discover(:return_response => true) 17 | end 18 | 19 | [status, headers, body] 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/get_attachment.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class GetAttachment < Middleware 5 | def action(env) 6 | params = env['params'] 7 | request_proxy_manager = env['request_proxy_manager'] 8 | 9 | if (params[:entity] != env['current_user'].entity) && !request_proxy_manager.can_read?(params[:entity]) 10 | halt!(404, "Not Found") 11 | end 12 | 13 | proxy_condition = if (params[:entity] == env['current_user'].entity) 14 | :never 15 | else 16 | request_proxy_manager.proxy_condition 17 | end 18 | 19 | unless proxy_condition == :always 20 | attachment, post_attachment = lookup_attachment(env) 21 | else 22 | attachment = nil 23 | end 24 | 25 | if !attachment && proxy_condition != :never && request_proxy_manager.can_proxy?(params[:entity]) 26 | # proxy request 27 | status, headers, body = request_proxy_manager.request(params[:entity]) do |client| 28 | client.attachment.get(params[:entity], params[:digest]) 29 | end 30 | return [status, headers, body] 31 | elsif !attachment 32 | halt!(404, "Not Found") 33 | end 34 | 35 | env['response'] = attachment 36 | (env['response.headers'] ||= {})['Content-Length'] = attachment.size.to_s 37 | env['response.headers']['Content-Type'] = post_attachment.content_type 38 | env 39 | end 40 | 41 | private 42 | 43 | def lookup_attachment(env) 44 | params = env['params'] 45 | 46 | attachment = nil 47 | 48 | attachment = Model::Attachment.find_by_digest(params[:digest]) 49 | return unless attachment 50 | 51 | posts = Model::Post. 52 | qualify. 53 | join(:posts_attachments, :posts__id => :posts_attachments__post_id). 54 | where(:posts_attachments__digest => attachment.digest, :posts__user_id => env['current_user'].id). 55 | order(Sequel.desc(:posts__received_at)). 56 | all 57 | return unless posts.any? 58 | 59 | unless post = posts.find { |post| Authorizer.new(env).read_authorized?(post) } 60 | return 61 | end 62 | 63 | post_attachment = Model::PostsAttachment.where(:post_id => post.id).first 64 | 65 | [attachment, post_attachment] 66 | end 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/get_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class GetPost < Middleware 5 | def action(env) 6 | if post = env['response.post'] 7 | halt!(404, "Not Found") unless post && Authorizer.new(env).read_authorized?(post) 8 | 9 | case env['HTTP_ACCEPT'] 10 | when MENTIONS_CONTENT_TYPE 11 | return ListPostMentions.new(@app).call(env) 12 | when CHILDREN_CONTENT_TYPE 13 | return ListPostChildren.new(@app).call(env) 14 | when VERSIONS_CONTENT_TYPE 15 | return ListPostVersions.new(@app).call(env) 16 | end 17 | end 18 | 19 | env 20 | end 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/hello_world.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class HelloWorld < Middleware 5 | def action(env) 6 | meta_post_url = TentD::Utils.expand_uri_template( 7 | env['current_user'].preferred_server['urls']['post'], 8 | :entity => env['current_user'].entity, 9 | :post => env['current_user'].meta_post.public_id 10 | ) 11 | 12 | headers = { 13 | 'Link' => %(<#{meta_post_url}>; rel="https://tent.io/rels/meta-post") 14 | } 15 | 16 | [200, headers, ['Tent!']] 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/list_post_children.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ListPostChildren < Middleware 5 | def action(env) 6 | ref_post = env.delete('response.post') 7 | 8 | q = Query.new(Model::Post) 9 | q.deleted_at_table_names = %w( posts ) 10 | 11 | q.query_conditions << "posts.user_id = ?" 12 | q.query_bindings << env['current_user'].id 13 | 14 | q.join("INNER JOIN parents ON parents.post_id = posts.id") 15 | 16 | q.query_conditions << "parents.parent_post_id = ?" 17 | q.query_bindings << ref_post.id 18 | 19 | authorizer = Authorizer.new(env) 20 | if env['current_auth'] && authorizer.auth_candidate 21 | unless authorizer.auth_candidate.read_all_types? 22 | _read_type_ids = Model::Type.find_types(authorizer.auth_candidate.read_types).inject({:base => [], :full => []}) do |memo, type| 23 | if type.fragment.nil? 24 | memo[:base] << type.id 25 | else 26 | memo[:full] << type.id 27 | end 28 | memo 29 | end 30 | 31 | q.query_conditions << ["OR", 32 | "posts.public = true", 33 | ["AND", 34 | "posts.entity_id = ?", 35 | ["OR", 36 | "posts.type_base_id IN ?", 37 | "posts.type_id IN ?" 38 | ] 39 | ] 40 | ] 41 | q.query_bindings << env['current_user'].entity_id 42 | q.query_bindings << _read_type_ids[:base] 43 | q.query_bindings << _read_type_ids[:full] 44 | end 45 | else 46 | q.query_conditions << "posts.public = true" 47 | end 48 | 49 | q.sort_columns = ["posts.version_received_at DESC"] 50 | 51 | q.limit = Feed::DEFAULT_PAGE_LIMIT 52 | 53 | children = q.all 54 | 55 | env['response'] = { 56 | :versions => children.map { |post| post.version_as_json(:env => env).merge(:type => post.type) } 57 | } 58 | 59 | if env['params']['profiles'] 60 | env['response'][:profiles] = MetaProfile.new(env, children).profiles( 61 | env['params']['profiles'].split(',') & ['entity'] 62 | ) 63 | end 64 | 65 | env['response.headers'] = {} 66 | env['response.headers']['Content-Type'] = CHILDREN_CONTENT_TYPE 67 | 68 | if env['REQUEST_METHOD'] == 'HEAD' 69 | env['response.headers']['Count'] = q.count.to_s 70 | end 71 | 72 | env 73 | end 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/list_post_mentions.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ListPostMentions < Middleware 5 | def action(env) 6 | ref_post = env.delete('response.post') 7 | 8 | q = Query.new(Model::Post) 9 | q.deleted_at_table_names = %w( posts ) 10 | 11 | q.select_columns = %w( posts.entity posts.entity_id posts.public_id posts.type mentions.public ) 12 | 13 | q.query_conditions << "posts.user_id = ?" 14 | q.query_bindings << env['current_user'].id 15 | 16 | q.join("INNER JOIN mentions ON mentions.post_id = posts.id") 17 | 18 | q.query_conditions << "mentions.post = ?" 19 | q.query_bindings << ref_post.public_id 20 | 21 | if env['current_auth'] && (auth_candidate = Authorizer::AuthCandidate.new(env['current_user'], env['current_auth.resource'])) && auth_candidate.read_type?(ref_post.type) 22 | q.query_conditions << "(mentions.public = true OR posts.entity_id = ?)" 23 | q.query_bindings << env['current_user'].entity_id 24 | else 25 | q.query_conditions << "mentions.public = true" 26 | end 27 | 28 | q.sort_columns = ["posts.received_at DESC"] 29 | 30 | q.limit = Feed::DEFAULT_PAGE_LIMIT 31 | 32 | posts = q.all 33 | 34 | env['response'] = { 35 | :mentions => posts.map { |post| 36 | m = { :type => post.type, :post => post.public_id } 37 | m[:entity] = post.entity unless ref_post.entity == post.entity 38 | m[:public] = false if post.public == false 39 | m 40 | } 41 | } 42 | 43 | if env['params']['profiles'] 44 | env['response'][:profiles] = MetaProfile.new(env, posts).profiles( 45 | env['params']['profiles'].split(',') & ['entity'] 46 | ) 47 | end 48 | 49 | env['response.headers'] = {} 50 | env['response.headers']['Content-Type'] = MENTIONS_CONTENT_TYPE 51 | 52 | if env['REQUEST_METHOD'] == 'HEAD' 53 | env['response.headers']['Count'] = q.count.to_s 54 | end 55 | 56 | env 57 | end 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/list_post_versions.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ListPostVersions < Middleware 5 | def action(env) 6 | ref_post = env.delete('response.post') 7 | 8 | q = Query.new(Model::Post) 9 | q.deleted_at_table_names = %w( posts ) 10 | 11 | q.query_conditions << "posts.user_id = ?" 12 | q.query_bindings << env['current_user'].id 13 | 14 | q.query_conditions << "posts.public_id = ?" 15 | q.query_bindings << ref_post.public_id 16 | 17 | authorizer = Authorizer.new(env) 18 | if env['current_auth'] && authorizer.auth_candidate 19 | unless authorizer.auth_candidate.read_all_types? 20 | _read_type_ids = Model::Type.find_types(authorizer.auth_candidate.read_types).inject({:base => [], :full => []}) do |memo, type| 21 | if type.fragment.nil? 22 | memo[:base] << type.id 23 | else 24 | memo[:full] << type.id 25 | end 26 | memo 27 | end 28 | 29 | q.query_conditions << ["OR", 30 | "posts.public = true", 31 | ["AND", 32 | "posts.entity_id = ?", 33 | ["OR", 34 | "posts.type_base_id IN ?", 35 | "posts.type_id IN ?" 36 | ] 37 | ] 38 | ] 39 | q.query_bindings << env['current_user'].entity_id 40 | q.query_bindings << _read_type_ids[:base] 41 | q.query_bindings << _read_type_ids[:full] 42 | end 43 | else 44 | q.query_conditions << "posts.public = true" 45 | end 46 | 47 | q.sort_columns = ["posts.version_received_at DESC"] 48 | 49 | q.limit = Feed::DEFAULT_PAGE_LIMIT 50 | 51 | versions = q.all 52 | 53 | env['response'] = { 54 | :versions => versions.map { |post| post.version_as_json(:env => env).merge(:type => post.type) } 55 | } 56 | 57 | if env['params']['profiles'] 58 | env['response'][:profiles] = MetaProfile.new(env, versions).profiles( 59 | env['params']['profiles'].split(',') & ['entity'] 60 | ) 61 | end 62 | 63 | env['response.headers'] = {} 64 | env['response.headers']['Content-Type'] = VERSIONS_CONTENT_TYPE 65 | 66 | if env['REQUEST_METHOD'] == 'HEAD' 67 | env['response.headers']['Count'] = q.count.to_s 68 | end 69 | 70 | env 71 | end 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/lookup_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class LookupPost < Middleware 5 | def action(env) 6 | params = env['params'] 7 | request_proxy_manager = env['request_proxy_manager'] 8 | 9 | proxy_condition = if (params[:entity] == env['current_user'].entity) || !['GET', 'HEAD'].include?(env['REQUEST_METHOD']) 10 | :never 11 | else 12 | request_proxy_manager.proxy_condition 13 | end 14 | 15 | post = unless proxy_condition == :always 16 | env['request.post_lookup_attempted'] = true 17 | 18 | if params['version'] && params['version'] != 'latest' 19 | Model::Post.first(:public_id => params[:post], :entity => params[:entity], :version => params['version']) 20 | else 21 | Model::Post.where(:public_id => params[:post], :entity => params[:entity]).order(Sequel.desc(:version_received_at)).first 22 | end 23 | end 24 | 25 | if !post && proxy_condition != :never && !env['request.post_list'] 26 | # proxy request 27 | status, headers, body = request_proxy_manager.request(params[:entity]) do |client| 28 | client.post.get(params[:entity], params[:post]) do |request| 29 | request.headers['Accept'] = env['HTTP_ACCEPT'] 30 | end 31 | end 32 | return [status, headers, body] 33 | else 34 | env['response.post'] = post 35 | end 36 | 37 | env 38 | end 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/not_found.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class NotFound < Middleware 5 | def action(env) 6 | halt!(404, "Not Found") 7 | end 8 | end 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/parse_content_type.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ParseContentType < Middleware 5 | 6 | def action(env) 7 | return env unless env['CONTENT_TYPE'] 8 | 9 | ## 10 | # Parse post type 11 | if env['CONTENT_TYPE'] =~ /\btype=['"]([^'"]+)['"]/ 12 | env['request.type'] = TentType.new($1) 13 | end 14 | 15 | ## 16 | # Parse rel 17 | env['request.rel'] = (env['CONTENT_TYPE'].match(/\brel=['"]([^'"]+)['"]/) || [])[1] 18 | 19 | case env['request.rel'] 20 | when "https://tent.io/rels/notification" 21 | env['request.notification'] = true 22 | when "https://tent.io/rels/import" 23 | env['request.import'] = true 24 | end 25 | 26 | ## 27 | # Parse type 28 | env['request.mime'] = env['CONTENT_TYPE'].to_s.split(';').first 29 | 30 | env 31 | end 32 | 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/parse_input_data.rb: -------------------------------------------------------------------------------- 1 | require 'yajl' 2 | 3 | module TentD 4 | class API 5 | 6 | class ParseInputData < Middleware 7 | 8 | def action(env) 9 | return env unless %w( POST PUT ).include?(env['REQUEST_METHOD']) 10 | 11 | if data = rack_input(env) 12 | env['data'] = case env['CONTENT_TYPE'].split(';').first 13 | when /json\Z/ 14 | Yajl::Parser.parse(data) 15 | when /\Amultipart/ 16 | post_matcher = Regexp.new("\\A#{Regexp.escape(POST_CONTENT_TYPE.split(';').first)}\\b") 17 | data = (env['data'].find { |k,v| v[:type] =~ post_matcher } || []).last 18 | env['attachments'] = env['data'].reject { |k,v| v[:type] =~ post_matcher }.inject([]) { |memo, (category, attachment)| 19 | attachment[:headers] = parse_headers(attachment.delete(:head)) 20 | attachment[:category] = category 21 | attachment[:name] = attachment[:filename] 22 | attachment[:content_type] = attachment.delete(:type) 23 | memo << attachment 24 | memo 25 | } 26 | env['CONTENT_TYPE'] = data[:type] if data 27 | data ? Yajl::Parser.parse(rack_input('rack.input' => data[:tempfile])) : nil 28 | else 29 | data 30 | end 31 | end 32 | 33 | env 34 | 35 | rescue Yajl::ParseError 36 | halt!(400, "Invalid JSON") 37 | end 38 | 39 | private 40 | 41 | def parse_headers(header_string) 42 | header_string.to_s.split(/$/).inject(Hash.new) do |memo, header| 43 | k,*v = header.chomp.strip.split(':') 44 | next memo unless k 45 | memo[k] = v.join.sub(/\A\s*/, '') 46 | memo 47 | end 48 | end 49 | 50 | end 51 | 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/parse_link_header.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ParseLinkHeader < Middleware 5 | 6 | def action(env) 7 | links = env['HTTP_LINK'].to_s.split(',') 8 | return env if links.empty? 9 | 10 | env['request.links'] = links.inject(Array.new) do |memo, link| 11 | parts = link.split(/;\s*/) 12 | url = parts.shift.to_s.slice(1...-1) # remove <> 13 | link = { :url => url } 14 | parts.each { |part| 15 | next unless part.downcase =~ /([a-z]+)=['"]([^'"]+)['"]/ 16 | key, val = $1, $2 17 | link[key.to_sym] = val 18 | } 19 | memo << link 20 | memo 21 | end 22 | 23 | env 24 | end 25 | 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/posts_feed.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class PostsFeed < Middleware 5 | def action(env) 6 | env['request.feed'] = true 7 | 8 | feed = Feed.new(env) 9 | 10 | env['response'] = feed 11 | 12 | if env['REQUEST_METHOD'] == 'HEAD' 13 | env['response.headers'] ||= {} 14 | env['response.headers']['Count'] = feed.count.to_s 15 | end 16 | 17 | env 18 | end 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/proxy_attachment_redirect.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ProxyAttachmentRedirect < Middleware 5 | def action(env) 6 | params = env['params'] 7 | request_proxy_manager = env['request_proxy_manager'] 8 | 9 | return env if params[:entity] == env['current_user'].entity 10 | return env if request_proxy_manager.proxy_condition == :never 11 | return env if request_proxy_manager.proxy_condition == :on_miss && !env['request.post_lookup_attempted'] 12 | 13 | proxy_client = request_proxy_manager.proxy_client(params[:entity], :skip_response_serialization => true) 14 | 15 | _params = Utils::Hash.slice(params, :version) 16 | res = proxy_client.post.get_attachment(params[:entity], params[:post], params[:name], _params) do |request| 17 | request.headers['Accept'] = env['HTTP_ACCEPT'] 18 | end 19 | 20 | body = res.body.respond_to?(:each) ? res.body : [res.body] 21 | 22 | if res.headers['Location'] 23 | digest = res.headers['Attachment-Digest'] 24 | headers = { 25 | 'Location' => "/attachments/#{URI.encode_www_form_component(params[:entity])}/#{digest}" 26 | } 27 | return [302, headers, []] 28 | else 29 | halt!(404, "Not Found") 30 | end 31 | rescue Faraday::Error::TimeoutError 32 | halt!(504, "Failed to proxy request: #{res.env[:method].to_s.upcase} #{res.env[:url].to_s}") 33 | rescue Faraday::Error::ConnectionFailed 34 | halt!(502, "Failed to proxy request: #{res.env[:method].to_s.upcase} #{res.env[:url].to_s}") 35 | end 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/proxy_post_list.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ProxyPostList < Middleware 5 | def action(env) 6 | unless [MENTIONS_CONTENT_TYPE, CHILDREN_CONTENT_TYPE, VERSIONS_CONTENT_TYPE].include?(env['HTTP_ACCEPT']) 7 | return env 8 | end 9 | 10 | env['request.post_list'] = true 11 | 12 | params = env['params'] 13 | request_proxy_manager = env['request_proxy_manager'] 14 | 15 | return env if params[:entity] == env['current_user'].entity 16 | return env if request_proxy_manager.proxy_condition == :never 17 | return env if request_proxy_manager.proxy_condition == :on_miss && !env['request.post_lookup_attempted'] 18 | 19 | _params = Utils::Hash.slice(params, :limit, :version) 20 | status, headers, body = request_proxy_manager.request(params[:entity]) do |client| 21 | client.post.get(params[:entity], params[:post], _params) do |request| 22 | request.headers['Accept'] = env['HTTP_ACCEPT'] 23 | end 24 | end 25 | return [status, headers, body] 26 | end 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/serve_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ServePost < Middleware 5 | def action(env) 6 | return env unless Model::Post === (post = env.delete('response.post')) 7 | 8 | params = env['params'] 9 | 10 | env['response'] = { 11 | :post => post.as_json(:env => env) 12 | } 13 | 14 | authorizer = Authorizer.new(env) 15 | 16 | if env['REQUEST_METHOD'] == 'GET' 17 | if params['max_refs'] && authorizer.app? 18 | env['response'][:refs] = Refs.new(env).fetch(post, params['max_refs'].to_i) 19 | end 20 | 21 | if params['profiles'] && authorizer.app? 22 | env['response'][:profiles] = MetaProfile.new(env, [post]).profiles(params['profiles'].split(',')) 23 | end 24 | end 25 | 26 | env['response.headers'] ||= {} 27 | env['response.headers']['Content-Type'] = POST_CONTENT_TYPE % post.type 28 | 29 | env 30 | end 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/set_request_proxy_manager.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class SetRequestProxyManager < Middleware 5 | def action(env) 6 | env['request_proxy_manager'] = RequestProxyManager.new(env) 7 | env 8 | end 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/user_lookup.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class UserLookup < Middleware 5 | EntityNotSetError = Class.new(StandardError) 6 | 7 | def action(env) 8 | unless env['current_user'] 9 | raise EntityNotSetError.new("You need to set ENV['TENT_ENTITY']!") unless ENV['TENT_ENTITY'] 10 | env['current_user'] = Model::User.first_or_create(ENV['TENT_ENTITY']) 11 | end 12 | env 13 | end 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/validate_input_data.rb: -------------------------------------------------------------------------------- 1 | require 'tentd/schema_validator' 2 | 3 | module TentD 4 | class API 5 | 6 | class ValidateInputData < Middleware 7 | def action(env) 8 | TentD.logger.debug "ValidateInputData#action" if TentD.settings[:debug] 9 | 10 | if Hash === env['data'] && [POST_CONTENT_MIME, MULTIPART_CONTENT_MIME].include?(env['request.mime']) 11 | validate_post!(env) 12 | validate_attachments!(env) 13 | end 14 | 15 | TentD.logger.debug "ValidateInputData#action complete" if TentD.settings[:debug] 16 | 17 | env 18 | end 19 | 20 | private 21 | 22 | def validate_post!(env) 23 | if env['data'].has_key?('content') && !(Hash === env['data']['content']) 24 | err = encode(env['data']['content']).inspect 25 | 26 | TentD.logger.debug "ValidateInputData Malformed content: #{err}" if TentD.settings[:debug] 27 | 28 | halt!(400, "Malformed content: #{err}") 29 | end 30 | 31 | unless env['data'].has_key?('type') 32 | err = encode(env['data']).inspect 33 | 34 | TentD.logger.debug "ValidateInputData type not specified: #{err}" if TentD.settings[:debug] 35 | 36 | halt!(400, "Type not specified: #{err}") 37 | end 38 | 39 | diff = SchemaValidator.diff(env['data']['type'], env['data']) 40 | if diff.any? 41 | TentD.logger.debug "ValidateInputData Invalid Attributes: #{diff}" if TentD.settings[:debug] 42 | 43 | halt!(400, "Invalid Attributes", :diff => diff) 44 | end 45 | end 46 | 47 | def validate_attachments!(env) 48 | return unless env['attachments'] 49 | 50 | TentD.logger.debug "ValidateInputData Malformed Request: env['attachments'] expected to be an array, got an instance of #{env['attachments'].class.name} instead" if TentD.settings[:debug] 51 | 52 | halt!(400, "Malformed Request") unless Array === env['attachments'] 53 | end 54 | 55 | def encode(data) 56 | Yajl::Encoder.encode(data) 57 | end 58 | end 59 | 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/tentd/api/middleware/validate_post_content_type.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class ValidatePostContentType < Middleware 5 | def action(env) 6 | unless valid_body?(env) 7 | halt!(400, "Request body missing.") 8 | end 9 | 10 | unless valid_content_type?(env) 11 | halt!(415, "Invalid Content-Type header. The header must be #{POST_CONTENT_MIME} or #{MULTIPART_CONTENT_MIME}.") 12 | end 13 | 14 | env 15 | end 16 | 17 | private 18 | 19 | def valid_body?(env) 20 | Hash === env['data'] 21 | end 22 | 23 | def valid_content_type?(env) 24 | post_type = env['data']['type'] 25 | ( 26 | env['CONTENT_TYPE'].to_s =~ Regexp.new("\\A#{Regexp.escape(POST_CONTENT_MIME)}\\b") && 27 | env['CONTENT_TYPE'].to_s =~ Regexp.new(%(\\btype=["']#{Regexp.escape(post_type)}["'])) 28 | ) || 29 | env['CONTENT_TYPE'].to_s =~ Regexp.new("\\A#{Regexp.escape(MULTIPART_CONTENT_MIME)}\\b") 30 | end 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tentd/api/notification_importer.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | class NotificationImporter 5 | def self.call(env) 6 | TentD.logger.debug "NotificationImporter.call" if TentD.settings[:debug] 7 | 8 | new(env).perform 9 | end 10 | 11 | attr_reader :env 12 | def initialize(env) 13 | @env = env 14 | end 15 | 16 | def perform 17 | TentD.logger.debug "NotificationImporter#perform" if TentD.settings[:debug] 18 | 19 | authorize! 20 | 21 | TentD.logger.debug "NotificationImporter -> Post.import_notification" if TentD.settings[:debug] 22 | 23 | env['response.post'] = Model::Post.import_notification(env) 24 | 25 | env 26 | rescue Model::Post::CreateFailure => e 27 | TentD.logger.debug "NotificationImporter: CreateFailure: #{e.inspect}" if TentD.settings[:debug] 28 | 29 | halt!(400, e.message) 30 | end 31 | 32 | private 33 | 34 | def halt!(status, message, attributes = {}) 35 | raise Middleware::Halt.new(status, message, attributes) 36 | end 37 | 38 | def authorize! 39 | authorizer = Authorizer.new(env) 40 | unless (Hash === env['data']) && authorizer.write_authorized?(env['data']['entity'], env['data']['type']) 41 | TentD.logger.debug "NotificationImporter: Unauthorized" if TentD.settings[:debug] 42 | 43 | halt!(403, "Unauthorized") 44 | end 45 | end 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/tentd/api/oauth.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | 4 | module OAuth 5 | require 'tentd/api/oauth/authorize' 6 | require 'tentd/api/oauth/token' 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tentd/api/oauth/authorize.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | module OAuth 4 | 5 | class Authorize < Middleware 6 | def action(env) 7 | ## 8 | # Only activate this endpoint when oauth_auth url points here (no app has taken responsibility) 9 | oauth_auth_url = "#{env['current_user'].entity}/oauth/authorize" 10 | halt!(404) unless env['current_user'].meta_post.content['servers'].any? do |server| 11 | server['urls']['oauth_auth'] == oauth_auth_url 12 | end 13 | 14 | halt!(400, "client_id missing") if env['params']['client_id'].to_s == "" 15 | 16 | app_post = Model::Post.first(:user_id => env['current_user'].id, :public_id => env['params']['client_id']) 17 | halt!(400, "invalid client_id") unless app_post 18 | 19 | app_auth_post = Model::AppAuth.create( 20 | env['current_user'], app_post, 21 | app_post.content['types'], 22 | app_post.content['scopes'] 23 | ) 24 | 25 | Model::AppAuth.update_app_post_refs(app_auth_post, app_post) 26 | 27 | credentials_post = nil 28 | app_auth_post.mentions.each do |m| 29 | type_base = Model::Type.find_or_create_base('https://tent.io/types/credentials/v0#') 30 | if _post = Model::Post.first(:user_id => env['current_user'].id, :public_id => m['post'], :type_base_id => type_base.id) 31 | credentials_post = _post 32 | break 33 | end 34 | end 35 | 36 | hawk_key = credentials_post.content['hawk_key'] 37 | code_param = "code=#{URI.encode_www_form_component(hawk_key)}" 38 | code_param += "&state=#{env['params']['state']}" if env['params']['state'] 39 | redirect_uri = URI(app_post.content['redirect_uri']) 40 | redirect_uri.query ? redirect_uri.query << "&#{code_param}" : redirect_uri.query = code_param 41 | 42 | env['response.headers'] ||= {} 43 | env['response.headers']['Location'] = redirect_uri.to_s 44 | env['response.status'] = 302 45 | 46 | env 47 | end 48 | end 49 | 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/tentd/api/oauth/token.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class API 3 | module OAuth 4 | 5 | class Token < Middleware 6 | def action(env) 7 | halt!(400, "Token code required!") unless Hash === env['data'] && env['data']['code'] 8 | token_code = env['data']['code'] 9 | 10 | unless app = Model::App.first(:user_id => env['current_user'].id, :auth_hawk_key => token_code) 11 | halt!(403, "Invalid token code") 12 | end 13 | 14 | unless env['current_auth.resource'] && (resource = env['current_auth.resource']) && TentType.new(resource.type).base == %(https://tent.io/types/app) && app.post.public_id == resource.public_id 15 | halt!(401, "Request must be signed using app credentials") 16 | end 17 | 18 | credentials_post = Model::Post.where(:id => app.auth_credentials_post_id).first 19 | credentials_post = Model::Credentials.refresh_key(credentials_post) 20 | 21 | env['response'] = { 22 | :access_token => credentials_post.public_id, 23 | :hawk_key => credentials_post.content['hawk_key'], 24 | :hawk_algorithm => credentials_post.content['hawk_algorithm'], 25 | :token_type => "https://tent.io/oauth/hawk-token" 26 | } 27 | 28 | env 29 | end 30 | end 31 | 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/tentd/api/relationship_initialization.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'yajl' 3 | require 'hawk' 4 | 5 | module TentD 6 | class API 7 | 8 | class RelationshipInitialization 9 | 10 | def self.call(env) 11 | new(env).perform 12 | end 13 | 14 | attr_reader :env, :current_user, :initiating_credentials_url, :initiating_credentials_post, :initiating_meta_post, :initiating_server, :initiating_entity_uri, :initiating_entity, :initiating_relationship_post 15 | def initialize(env) 16 | @env = env 17 | @current_user = env['current_user'] 18 | end 19 | 20 | ## 21 | # Perform relationship initiation steps, 22 | # return 400 response if/when any of the steps fail 23 | def perform 24 | ## 25 | # Ensure relationship post is valid 26 | # (ValidateInputData middleware will have validated it against the schema already if in correct format) 27 | unless Hash === env['data'] 28 | halt!(400, "Invalid relationship post!", :post => env['data']) 29 | end 30 | 31 | ## 32 | # relationship#initial post 33 | @initiating_relationship_post = Utils::Hash.symbolize_keys(env['data']) 34 | 35 | ## 36 | # Entity of server initiating relationship 37 | @initiating_entity_uri = env['data']['entity'] 38 | @initiating_entity = Model::Entity.first_or_create(initiating_entity_uri) 39 | 40 | ## 41 | # Fetch meta post from initiating server via discovery 42 | @initiating_meta_post = perform_discovery 43 | 44 | ## 45 | # Ensure we have the correct meta post 46 | meta = initiating_meta_post 47 | unless (Hash === meta) && (Hash === meta['content']) && meta['content']['entity'] == initiating_entity_uri 48 | patch = { :op => "replace", :path => "/content/entity", :value => initiating_entity_uri } 49 | 50 | if !(Hash === meta) || !(Hash === meta['content']) || !meta['content'].has_key?('entity') 51 | patch[:op] = "add" 52 | end 53 | 54 | halt!(400, "Entity mismatch!", :diff => [patch], :post => meta) 55 | end 56 | 57 | ## 58 | # Parse signed credentials url for relationship post from Link header 59 | @initiating_credentials_url = parse_credentials_link 60 | 61 | ## 62 | # Find preferred server with host matching credentials url 63 | @initiating_server = select_initiating_server 64 | 65 | ## 66 | # Fetch credentials post from initiating server 67 | @initiating_credentials_post = fetch_initiating_credentials_post 68 | 69 | ## 70 | # Verify relationship#initial post exists on initiating server 71 | # and is accessible via fetched credentials 72 | verify_relationship! 73 | 74 | ## 75 | # Store initiating meta post 76 | remote_meta_post = save_initiating_post(Utils::Hash.symbolize_keys(initiating_meta_post)) 77 | 78 | ## 79 | # Store credentials post for initial relationship post 80 | save_initiating_post(initiating_credentials_post) 81 | 82 | ## 83 | # Create new relationship post (without fragment) mentioning initial relationship post 84 | # and credentials post mentioning new relationship post 85 | relationship = Model::Relationship.create_final(current_user, 86 | :remote_relationship => initiating_relationship_post, 87 | :remote_credentials => initiating_credentials_post, 88 | :remote_meta_post => remote_meta_post 89 | ) 90 | 91 | ## 92 | # Link credentials post for new relationship post in response header 93 | credentials_url = Utils.expand_uri_template(current_user.preferred_server['urls']['post'], 94 | :entity => current_user.entity, 95 | :post => relationship.credentials_post.public_id 96 | ) 97 | env['response.links'] ||= [] 98 | env['response.links'].push( 99 | :url => Utils.sign_url(current_user.server_credentials, credentials_url), 100 | :rel => CREDENTIALS_LINK_REL 101 | ) 102 | 103 | ## 104 | # Respond with initial relationship post (request payload) 105 | env['response'] = { 106 | :post => env['data'] 107 | } 108 | env['response.headers'] ||= {} 109 | env['response.headers']['Content-Type'] = POST_CONTENT_TYPE % env['data']['type'] 110 | 111 | env 112 | end 113 | 114 | private 115 | 116 | def halt!(status, message, attributes = {}) 117 | raise Middleware::Halt.new(status, message, attributes) 118 | end 119 | 120 | def parse_credentials_link 121 | unless link = env['request.links'].to_a.find { |link| link[:rel] == CREDENTIALS_LINK_REL } 122 | halt!(400, "Expected link to credentials post!") 123 | end 124 | 125 | link[:url] 126 | end 127 | 128 | def perform_discovery 129 | unless meta = TentClient.new(initiating_entity_uri).server_meta_post 130 | halt!(400, "Discovery of entity #{initiating_entity_uri.inspect} failed!") 131 | end 132 | 133 | meta 134 | end 135 | 136 | def select_initiating_server 137 | # Sort servers by preference (lowest first) 138 | sorted_servers = initiating_meta_post['content']['servers'].sort_by { |s| s['preference'] } 139 | 140 | # Find server with matching host 141 | uri = URI(initiating_credentials_url) 142 | server = sorted_servers.find { |server| 143 | post_uri = URI(server['urls']['post'].gsub(/[{}]/, '')) 144 | 145 | (post_uri.scheme == uri.scheme) && (post_uri.host == uri.host) && (post_uri.port == uri.port) 146 | } 147 | 148 | unless server 149 | port_regex = [443, 80].include?(uri.port) ? "" : ":#{uri.port}" 150 | diff = [{ 151 | :op => "add", 152 | :path => "/content/servers/urls/~/post", 153 | :value => "/^#{Regexp.escape(uri.scheme)}:\/\/#{Regexp.escape(uri.host)}#{port_regex}/", 154 | :type => "regexp" 155 | }] 156 | halt!(400, "Matching server not found!", :diff => diff, :post => initiating_meta_post) 157 | end 158 | 159 | server 160 | end 161 | 162 | def fetch_initiating_credentials_post 163 | res = Faraday.get(initiating_credentials_url) do |request| 164 | request.headers['Accept'] = POST_CONTENT_TYPE % CREDENTIALS_MIME_TYPE 165 | end 166 | 167 | wrapped_post = Utils::Hash.symbolize_keys(Yajl::Parser.parse(res.body)) 168 | post = wrapped_post[:post] 169 | 170 | unless TentType.new(post[:type]).base == TentType.new(CREDENTIALS_MIME_TYPE).base 171 | if wrapped_post.has_key?(:error) 172 | halt!(400, "Invalid credentials post! (#{wrapped_post[:error]})", wrapped_post) 173 | else 174 | halt!(400, "Invalid credentials post!", post) 175 | end 176 | end 177 | 178 | diff = SchemaValidator.diff(post[:type], Utils::Hash.stringify_keys(post)) 179 | unless diff.empty? 180 | halt!(400, "Invalid credentials post format!", :diff => diff, :post => post) 181 | end 182 | 183 | post 184 | rescue Faraday::Error::TimeoutError, Faraday::Error::ConnectionFailed 185 | halt!(400, "Failed to fetch credentials post from #{initiating_credentials_url.inspect}!") 186 | rescue Yajl::ParseError 187 | halt!(400, "Invalid credentials post encoding!", :post => res.body) 188 | end 189 | 190 | def verify_relationship! 191 | post = initiating_relationship_post 192 | 193 | # use the same server as credentials post 194 | _meta_post = Utils::Hash.deep_dup(initiating_meta_post) 195 | _meta_post['content']['servers'] = [initiating_server] 196 | 197 | client = TentClient.new(initiating_entity_uri, 198 | :server_meta => _meta_post, 199 | :credentials => { 200 | :id => initiating_credentials_post[:id], 201 | :hawk_key => initiating_credentials_post[:content][:hawk_key], 202 | :hawk_algorithm => initiating_credentials_post[:content][:hawk_algorithm] 203 | } 204 | ) 205 | 206 | res = client.post.get(post[:entity], post[:id]) do |request| 207 | request.headers['Accept'] = POST_CONTENT_TYPE % post[:type] 208 | end 209 | 210 | unless res.status == 200 211 | halt!(400, "Failed to fetch relationship post from #{res.env[:url].to_s.inspect}!", 212 | :response_status => res.status, 213 | :response_body => res.body 214 | ) 215 | end 216 | end 217 | 218 | def save_initiating_post(data) 219 | Model::PostBuilder.create_from_env( 220 | { 221 | 'current_user' => current_user, 222 | 'data' => Utils::Hash.stringify_keys(data).merge( 223 | 'version' => data[:version][:id], 224 | ) 225 | }, 226 | :notification => true, 227 | :public_id => data[:id], 228 | :entity => initiating_entity_uri, 229 | :entity_id => initiating_entity.id 230 | ) 231 | end 232 | end 233 | 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /lib/tentd/api/serialize_response.rb: -------------------------------------------------------------------------------- 1 | require 'yajl' 2 | 3 | module TentD 4 | class API 5 | 6 | module SerializeResponse 7 | extend self 8 | 9 | def call(env) 10 | response_headers = build_response_headers(env) 11 | if env.has_key?('response') && env['response'] 12 | response_body = env['response'] 13 | if !response_body.respond_to?(:each) && response_body.respond_to?(:as_json) 14 | response_body = response_body.as_json(:env => env) 15 | end 16 | 17 | response_headers = { 18 | 'Content-Type' => content_type(response_body) 19 | }.merge(response_headers) 20 | 21 | response_body = serialize(response_body) 22 | 23 | if String === response_body 24 | response_body = [response_body] 25 | end 26 | 27 | [env['response.status'] || 200, response_headers, response_body] 28 | else 29 | if env['response.status'] 30 | [env['response.status'], {}.merge(response_headers), []] 31 | else 32 | [404, { 'Content-Type' => ERROR_CONTENT_TYPE }.merge(response_headers), [serialize(:error => 'Not Found')]] 33 | end 34 | end 35 | end 36 | 37 | private 38 | 39 | def build_response_headers(env) 40 | headers = (env['response.headers'] || Hash.new) 41 | 42 | links = (env['response.links'] || Array.new).map do |link| 43 | _link = "<#{link.delete(:url)}>" 44 | if link.keys.any? 45 | _link << link.inject([nil]) { |memo, (k,v)| memo << %(#{k}=#{v.inspect}); memo }.join("; ") 46 | end 47 | _link 48 | end 49 | 50 | if links.any? 51 | links = links.join(', ') 52 | headers['Link'] ? headers['Link'] << ", #{links}" : headers['Link'] = links 53 | end 54 | 55 | headers 56 | end 57 | 58 | def content_type(response_body) 59 | return "" unless Hash === response_body 60 | if type = response_body[:type] 61 | POST_CONTENT_TYPE % (Hash === response_body ? type : "") 62 | else 63 | %(application/json) 64 | end 65 | end 66 | 67 | def serialize(response_body) 68 | if Hash === response_body 69 | Yajl::Encoder.encode(response_body) 70 | else 71 | response_body 72 | end 73 | end 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tentd/authorizer.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | 3 | class Authorizer 4 | 5 | require 'tentd/authorizer/auth_candidate' 6 | 7 | attr_reader :env 8 | def initialize(env) 9 | @env = env 10 | end 11 | 12 | def current_user 13 | env['current_user'] 14 | end 15 | 16 | def auth_candidate 17 | return @auth_candidate if @auth_candidate 18 | 19 | return unless env['current_auth.resource'] 20 | 21 | @auth_candidate ||= AuthCandidate.new(current_user, env['current_auth.resource']) 22 | end 23 | 24 | def app_json 25 | candidate = auth_candidate 26 | 27 | app_post = case candidate 28 | when AuthCandidate::App 29 | candidate.resource 30 | when AuthCandidate::AppAuth 31 | if (_app = Model::App.where(:auth_post_id => candidate.resource.id).first) 32 | _app.post 33 | end 34 | end 35 | 36 | if app_post 37 | { 38 | :name => app_post.content['name'], 39 | :url => app_post.content['url'], 40 | :id => app_post.public_id 41 | } 42 | end 43 | end 44 | 45 | def app? 46 | return false unless env['current_auth'] 47 | 48 | # Private server credentials have same permissions as fully authorized app 49 | return true if env['current_auth'][:credentials_resource] == current_user 50 | 51 | return false unless resource = env['current_auth.resource'] 52 | 53 | TentType.new(resource.type).base == %(https://tent.io/types/app-auth) 54 | end 55 | 56 | def read_authorized?(post) 57 | return true if post.public 58 | return false unless env['current_auth'] 59 | 60 | # Private server credentials have full authorization 61 | return true if env['current_auth'][:credentials_resource] == current_user 62 | 63 | # Credentials aren't linked to a valid resource 64 | return false unless auth_candidate 65 | 66 | auth_candidate.read_type?(post.type) || auth_candidate.read_post?(post) 67 | end 68 | 69 | def write_post?(post) 70 | return false unless env['current_auth'] 71 | 72 | # Private server credentials have full authorization 73 | return true if env['current_auth'][:credentials_resource] == current_user 74 | 75 | # Credentials aren't linked to a valid resource 76 | return false unless auth_candidate 77 | 78 | auth_candidate.write_post?(post) 79 | end 80 | 81 | def write_post_id?(entity, public_id, post_type) 82 | return false unless env['current_auth'] 83 | 84 | # Private server credentials have full authorization 85 | return true if env['current_auth'][:credentials_resource] == current_user 86 | 87 | # Credentials aren't linked to a valid resource 88 | return false unless auth_candidate 89 | 90 | if env['request.import'] 91 | auth_candidate.has_scope?('import') && auth_candidate.write_type?(post_type) 92 | else 93 | auth_candidate.write_post_id?(entity, public_id, post_type) 94 | end 95 | end 96 | 97 | def write_authorized?(entity, post_type) 98 | return false unless env['current_auth'] 99 | 100 | # Private server credentials have full authorization 101 | return true if env['current_auth'][:credentials_resource] == current_user 102 | 103 | # Credentials aren't linked to a valid resource 104 | return false unless auth_candidate 105 | 106 | if env['request.import'] 107 | auth_candidate.has_scope?('import') && auth_candidate.write_type?(post_type) 108 | else 109 | auth_candidate.write_entity?(entity) && auth_candidate.write_type?(post_type) 110 | end 111 | end 112 | 113 | def can_set_permissions? 114 | return true if Hash === env['data'] && env['data']['entity'] != current_user.entity 115 | 116 | # Credentials aren't linked to a valid resource 117 | return false unless auth_candidate 118 | 119 | # Private server credentials have full authorization 120 | return true if env['current_auth'][:credentials_resource] == current_user 121 | 122 | auth_candidate.has_scope?('permissions') 123 | end 124 | 125 | def proxy_authorized? 126 | return false unless env['current_auth'] 127 | 128 | # Private server credentials have full authorization 129 | return true if env['current_auth'][:credentials_resource] == current_user 130 | 131 | # Credentials aren't linked to a valid resource 132 | return false unless auth_candidate 133 | 134 | TentType.new(auth_candidate.resource.type).base == %(https://tent.io/types/app-auth) 135 | end 136 | 137 | def read_entity?(entity) 138 | return true if entity == current_user.entity 139 | 140 | return false unless env['current_auth'] 141 | 142 | # Private server credentials have full authorization 143 | return true if env['current_auth'][:credentials_resource] == current_user 144 | 145 | # Credentials aren't linked to a valid resource 146 | return false unless auth_candidate 147 | 148 | auth_candidate.read_entity?(entity) 149 | end 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /lib/tentd/authorizer/auth_candidate.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Authorizer 3 | 4 | module AuthCandidate 5 | require 'tentd/authorizer/auth_candidate/base' 6 | require 'tentd/authorizer/auth_candidate/app_auth' 7 | require 'tentd/authorizer/auth_candidate/app' 8 | require 'tentd/authorizer/auth_candidate/relationship' 9 | 10 | def self.new(current_user, resource) 11 | case TentType.new(resource.type).base 12 | when %(https://tent.io/types/app-auth) 13 | AppAuth.new(current_user, resource) 14 | when %(https://tent.io/types/app) 15 | App.new(current_user, resource) 16 | when %(https://tent.io/types/relationship) 17 | Relationship.new(current_user, resource) 18 | else 19 | Base.new(current_user, resource) 20 | end 21 | end 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/tentd/authorizer/auth_candidate/app.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Authorizer 3 | module AuthCandidate 4 | 5 | class App < Base 6 | def read_post?(post) 7 | post == resource 8 | end 9 | 10 | def write_post?(post) 11 | post == resource 12 | end 13 | 14 | def write_post_id?(entity, public_id, type_uri) 15 | entity == resource.entity && public_id == resource.public_id && type_uri == resource.type 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tentd/authorizer/auth_candidate/app_auth.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Authorizer 3 | module AuthCandidate 4 | 5 | class AppAuth < Base 6 | def read_types 7 | @read_types ||= resource.content['types']['read'].to_a 8 | end 9 | 10 | def read_type?(type_uri) 11 | return true if read_all_types? 12 | type_authorized?(type_uri, read_types) || write_type?(type_uri) 13 | end 14 | 15 | def read_all_types? 16 | read_types.any? { |t| t == 'all' } 17 | end 18 | 19 | def read_entity?(entity) 20 | true 21 | end 22 | 23 | def write_entity?(entity) 24 | return true if entity.nil? # entity not included in request body 25 | return true if resource.content['scopes'].to_a.find { |s| s == 'import' } 26 | entity == current_user.entity 27 | end 28 | 29 | def write_post?(post) 30 | write_type?(post.type) 31 | end 32 | 33 | def write_types 34 | resource.content['types']['write'].to_a 35 | end 36 | 37 | def write_all_types? 38 | write_types.any? { |t| t == 'all' } 39 | end 40 | 41 | def write_type?(type_uri) 42 | return true if write_all_types? 43 | type_authorized?(type_uri, write_types) 44 | end 45 | 46 | def scopes 47 | resource.content['scopes'].to_a 48 | end 49 | 50 | def has_scope?(scope) 51 | scopes.include?(scope.to_s) 52 | end 53 | end 54 | 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/tentd/authorizer/auth_candidate/base.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Authorizer 3 | module AuthCandidate 4 | 5 | class Base 6 | attr_reader :current_user, :resource 7 | def initialize(current_user, resource) 8 | @current_user, @resource = current_user, resource 9 | end 10 | 11 | def read_types 12 | [] 13 | end 14 | 15 | def read_type?(type_uri) 16 | false 17 | end 18 | 19 | def read_post?(post) 20 | false 21 | end 22 | 23 | def read_all_types? 24 | false 25 | end 26 | 27 | def read_entity?(entity_uri) 28 | false 29 | end 30 | 31 | def write_types 32 | [] 33 | end 34 | 35 | def write_type?(type_uri) 36 | false 37 | end 38 | 39 | def write_post?(post) 40 | false 41 | end 42 | 43 | def write_post_id?(entity, public_id, type_uri) 44 | write_entity?(entity) && write_type?(type_uri) 45 | end 46 | 47 | def write_all_types? 48 | false 49 | end 50 | 51 | def write_entity?(entity_uri) 52 | false 53 | end 54 | 55 | def has_scope?(scope) 56 | false 57 | end 58 | 59 | private 60 | 61 | def type_authorized?(type_uri, authorized_type_uris) 62 | type = TentType.new(type_uri) 63 | authorized_type_uris.any? do |authorized_type_uri| 64 | break true if authorized_type_uri == type_uri 65 | 66 | authorized_type = TentType.new(authorized_type_uri) 67 | 68 | break true if !authorized_type.has_fragment? && authorized_type.base == type.base 69 | 70 | authorized_type.base == type.base && authorized_type.fragment == type.fragment 71 | end 72 | end 73 | end 74 | 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/tentd/authorizer/auth_candidate/relationship.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Authorizer 3 | module AuthCandidate 4 | 5 | class Relationship < Base 6 | def read_post?(post) 7 | post == resource 8 | end 9 | 10 | def write_entity?(entity_uri) 11 | return false unless relationship 12 | relationship.entity == entity_uri 13 | end 14 | 15 | def write_type?(type_uri) 16 | return false unless relationship 17 | true 18 | end 19 | 20 | private 21 | 22 | def relationship 23 | @relationship ||= Model::Relationship.where(:post_id => resource.id).first 24 | end 25 | end 26 | 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/tentd/feed.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | 3 | class Feed 4 | 5 | DEFAULT_PAGE_LIMIT = 25.freeze 6 | MAX_PAGE_LIMIT = 200.freeze 7 | 8 | require 'tentd/feed/pagination' 9 | 10 | attr_reader :env 11 | attr_accessor :check_beyond_limit, :beyond_limit_exists 12 | def initialize(env) 13 | @env = env 14 | end 15 | 16 | def build_params 17 | params = env['params'] 18 | 19 | if params['entities'] 20 | params['entities'] = params['entities'].split(',').uniq.select { |entity| 21 | authorizer.read_entity?(entity) 22 | } 23 | end 24 | 25 | params 26 | end 27 | 28 | def params 29 | @params ||= build_params 30 | end 31 | 32 | def current_user 33 | env['current_user'] 34 | end 35 | 36 | def authorizer 37 | Authorizer.new(env) 38 | end 39 | 40 | def entities 41 | return unless params['entities'] 42 | end 43 | 44 | def limit 45 | _limit = if params['limit'] 46 | [params['limit'].to_i, MAX_PAGE_LIMIT].min 47 | else 48 | DEFAULT_PAGE_LIMIT 49 | end 50 | 51 | if params['max_refs'] && authorizer.app? 52 | _limit = [MAX_PAGE_LIMIT / [Refs::MAX_REFS_PER_POST, params['max_refs'].to_i].min, _limit].min 53 | end 54 | 55 | _limit 56 | end 57 | 58 | def build_query(params = send(:params)) 59 | q = _build_query(params) 60 | 61 | if authorizer.app? 62 | read_types = authorizer.auth_candidate.read_types | authorizer.auth_candidate.write_types 63 | 64 | unless read_types == %w( all ) 65 | read_types.map! { |uri| TentType.new(uri) } 66 | authorized_base_types = read_types.select { |t| !t.has_fragment? } 67 | authorized_base_type_ids = Model::Type.find_types(authorized_base_types).map(&:id) 68 | authorized_types_with_fragments = read_types.select { |t| t.has_fragment? } 69 | authorized_type_ids_with_fragments = Model::Type.find_types(authorized_types_with_fragments).map(&:id) 70 | 71 | _condition = ["OR", "#{q.table_name}.public = true"] 72 | 73 | if authorized_base_type_ids.any? 74 | _condition << "#{q.table_name}.type_base_id IN ?" 75 | q.query_bindings << authorized_base_type_ids 76 | end 77 | 78 | if authorized_type_ids_with_fragments.any? 79 | _condition << "#{q.table_name}.type_id IN ?" 80 | q.query_bindings << authorized_type_ids_with_fragments 81 | end 82 | 83 | q.query_conditions << _condition 84 | end 85 | else 86 | q.query_conditions << "#{q.table_name}.public = true" 87 | end 88 | 89 | q 90 | end 91 | 92 | def _build_query(params) 93 | q = Query.new(Model::Post) 94 | q.deleted_at_table_names = %w( posts ) 95 | 96 | # TODO: handle sort columns/order better 97 | sort_columns = case params['sort_by'] 98 | when 'published_at' 99 | ["#{q.table_name}.published_at DESC"] 100 | when 'version.published_at' 101 | ["#{q.table_name}.version_published_at DESC"] 102 | else 103 | ["#{q.table_name}.received_at DESC"] 104 | end 105 | q.sort_columns = sort_columns 106 | 107 | q.query_conditions << "#{q.table_name}.user_id = ?" 108 | q.query_bindings << env['current_user'].id 109 | 110 | timestamp_column = q.sort_columns.split(' ').first 111 | 112 | q.query_conditions << "#{q.table_name}.version = ( 113 | SELECT version FROM #{q.table_name} AS tmp 114 | WHERE tmp.public_id = #{q.table_name}.public_id 115 | ORDER BY received_at DESC LIMIT 1 116 | )" 117 | 118 | if params['since'] 119 | since_timestamp, since_version = params['since'].split(' ') 120 | since_timestamp = since_timestamp.to_i 121 | 122 | q.reverse_sort = true 123 | 124 | if since_version 125 | q.query_conditions << ["OR", 126 | ["AND", "#{timestamp_column} >= ?", "#{q.table_name}.version > ?"], 127 | "#{timestamp_column} > ?" 128 | ] 129 | q.query_bindings << since_timestamp 130 | q.query_bindings << since_version 131 | q.query_bindings << since_timestamp 132 | 133 | sort_columns << "#{q.table_name}.version DESC" 134 | q.sort_columns = sort_columns 135 | else 136 | q.query_conditions << "#{timestamp_column} > ?" 137 | q.query_bindings << since_timestamp 138 | end 139 | end 140 | 141 | if params['until'] 142 | until_timestamp, until_version = params['until'].split(' ') 143 | until_timestamp = until_timestamp.to_i 144 | 145 | if until_version 146 | q.query_conditions << ["OR", 147 | ["AND", "#{timestamp_column} >= ?", "#{q.table_name}.version > ?"], 148 | "#{timestamp_column} > ?" 149 | ] 150 | q.query_bindings << until_timestamp 151 | q.query_bindings << until_version 152 | q.query_bindings << until_timestamp 153 | 154 | sort_columns << "#{q.table_name}.version DESC" 155 | q.sort_columns = sort_columns 156 | else 157 | q.query_conditions << "#{timestamp_column} > ?" 158 | q.query_bindings << until_timestamp 159 | end 160 | end 161 | 162 | if params['before'] 163 | before_timestamp, before_version = params['before'].split(' ') 164 | before_timestamp = before_timestamp.to_i 165 | 166 | if before_version 167 | q.query_conditions << ["OR", 168 | ["AND", "#{timestamp_column} <= ?", "#{q.table_name}.version < ?"], 169 | "#{timestamp_column} < ?" 170 | ] 171 | q.query_bindings << before_timestamp 172 | q.query_bindings << before_version 173 | q.query_bindings << before_timestamp 174 | 175 | sort_columns << "#{q.table_name}.version DESC" 176 | q.sort_columns = sort_columns 177 | else 178 | q.query_conditions << "#{timestamp_column} < ?" 179 | q.query_bindings << before_timestamp 180 | end 181 | end 182 | 183 | if params['types'] 184 | tent_types = params['types'].to_s.split(",").uniq.map { |uri| TentType.new(uri) } 185 | tent_types_without_fragment = tent_types.select { |t| !t.has_fragment? } 186 | tent_types_with_fragment = tent_types.select { |t| t.has_fragment? } 187 | 188 | base_type_ids = Model::Type.where(:base => tent_types_without_fragment.map(&:base), :fragment => nil).map(&:id) 189 | full_type_ids = Model::Type.where(:base => tent_types_with_fragment.map(&:base), :fragment => tent_types_with_fragment.map { |t| t.fragment.to_s }).map(&:id) 190 | 191 | q.query_conditions << ["OR", "#{q.table_name}.type_base_id IN ?", "#{q.table_name}.type_id IN ?"] 192 | q.query_bindings << base_type_ids 193 | q.query_bindings << full_type_ids 194 | end 195 | 196 | if params['entities'] 197 | q.query_conditions << "#{q.table_name}.entity IN ?" 198 | q.query_bindings << params['entities'] 199 | end 200 | 201 | if params['mentions'] 202 | mentions_table = Model::Mention.table_name 203 | q.join("INNER JOIN #{mentions_table} ON #{mentions_table}.post_id = #{q.table_name}.id") 204 | 205 | mentions = Array(params['mentions']).map do |mentions_param| 206 | mentions_param.split(',').map do |mention| 207 | entity, post = mention.split(' ') 208 | mention = { :entity => entity } 209 | mention[:post] = post if post 210 | mention 211 | end 212 | end 213 | 214 | # fetch entity ids 215 | flat_mentions = mentions.flatten 216 | entities = flat_mentions.map { |mention| mention[:entity] } 217 | entities_q = Query.new(Model::Entity) 218 | entities_q.query_conditions << "entity IN ?" 219 | entities_q.query_bindings << entities 220 | _entity_mapping = {} 221 | entities_q.all.each do |entity| 222 | _entity_mapping[entity.entity] = entity.id 223 | end 224 | flat_mentions.each do |m| 225 | m[:entity_id] = _entity_mapping[m[:entity]] 226 | end 227 | 228 | mentions.each do |_mentions| 229 | mentions_bindings = [] 230 | mentions_conditions = ['OR'].concat(_mentions.map { |mention| 231 | mentions_bindings << mention[:entity_id] 232 | if mention[:post] 233 | mentions_bindings << mention[:post] 234 | "(#{mentions_table}.entity_id = ? AND #{mentions_table}.post = ?)" 235 | else 236 | "#{mentions_table}.entity_id = ?" 237 | end 238 | }) 239 | 240 | q.query_conditions << mentions_conditions 241 | q.query_bindings.push(*mentions_bindings) 242 | end 243 | end 244 | 245 | q.limit = limit 246 | 247 | unless params['since'] 248 | q.limit += 1 249 | self.check_beyond_limit = true 250 | end 251 | 252 | q 253 | end 254 | 255 | def count 256 | build_query.count 257 | end 258 | 259 | def fetch_query 260 | _params = Utils::Hash.deep_dup(params) 261 | 262 | if _params['entities'] && request_proxy_manager.proxy_condition == :always 263 | # separate entities to be proxied 264 | _proxy_entities = _params['entities'] - [current_user.entity] 265 | _params['entities'] = _params['entities'] - _proxy_entities 266 | 267 | @models = merge_results(build_query(_params).all, fetch_via_proxy(_proxy_entities)) 268 | else 269 | @models = build_query(_params).all 270 | end 271 | 272 | if check_beyond_limit 273 | if @models.size == limit + 1 274 | @models.pop 275 | self.beyond_limit_exists = true 276 | else 277 | self.beyond_limit_exists = false 278 | end 279 | end 280 | 281 | @models 282 | end 283 | 284 | def fetch_via_proxy(entities) 285 | posts = [] 286 | entities.each do |entity| 287 | client = request_proxy_manager.proxy_client(entity) 288 | res = client.post.list(params.merge('entities' => entity)) 289 | if res.status == 200 290 | posts.concat res.body['posts'] 291 | end 292 | end 293 | posts.map { |post| ProxiedPost.new(post) } 294 | end 295 | 296 | def merge_results(models, proxied_posts) 297 | (models + proxied_posts).sort_by do |item| 298 | case params['sort_by'] 299 | when 'published_at' 300 | item.published_at 301 | when 'version.published_at' 302 | item.version_published_at 303 | else 304 | item.received_at || item.published_at 305 | end 306 | end 307 | end 308 | 309 | def models 310 | @models || fetch_query 311 | end 312 | 313 | def as_json(options = {}) 314 | _models = models 315 | res = { 316 | :pages => Pagination.new(self).as_json, 317 | :posts => _models.map { |m| m.as_json(:env => env) } 318 | } 319 | 320 | if params['max_refs'] && authorizer.app? 321 | res[:refs] = Refs.new(env).fetch(*_models, params['max_refs'].to_i) 322 | end 323 | 324 | if params['profiles'] && authorizer.app? 325 | res[:profiles] = API::MetaProfile.new(env, _models).profiles(params['profiles'].split(',')) 326 | end 327 | 328 | res 329 | end 330 | 331 | private 332 | 333 | def request_proxy_manager 334 | @request_proxy_manager ||= env['request_proxy_manager'] 335 | end 336 | end 337 | 338 | end 339 | -------------------------------------------------------------------------------- /lib/tentd/feed/pagination.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class Feed 3 | 4 | class Pagination 5 | attr_reader :feed 6 | def initialize(feed) 7 | # feed.models is a mixture of Model::Post and TentD::ProxiedPost 8 | 9 | @feed = feed 10 | end 11 | 12 | def base_params 13 | @base_params ||= begin 14 | params = feed.params.dup 15 | %w( before since until ).each { |k| params.delete(k) } 16 | params 17 | end 18 | end 19 | 20 | def prev_posts_exist? 21 | return false unless feed.models.any? 22 | 23 | %w( before since ).any? { |k| feed.params.keys.include?(k) } 24 | end 25 | 26 | def next_posts_exist? 27 | return false unless feed.models.any? 28 | return false if feed.params['since'].to_s == '0' 29 | 30 | if feed.params['since'] 31 | params = base_params.dup 32 | 33 | before_post = feed.models.last 34 | params['before'] = [before_post.published_at, before_post.version].join(' ') 35 | params['since'] = '0' 36 | 37 | q = feed.build_query(params) 38 | q.any? 39 | else 40 | feed.beyond_limit_exists == true 41 | end 42 | end 43 | 44 | def first_params 45 | return unless prev_posts_exist? 46 | 47 | params = base_params.dup 48 | 49 | until_post = feed.models.first 50 | params['until'] = [until_post.published_at, until_post.version].join(' ') 51 | 52 | params 53 | end 54 | 55 | def last_params 56 | return unless next_posts_exist? 57 | 58 | params = base_params.dup 59 | 60 | params['since'] = 0 61 | 62 | before_post = feed.models.last 63 | params['before'] = [before_post.published_at, before_post.version].join(' ') 64 | 65 | params 66 | end 67 | 68 | def next_params 69 | return unless next_posts_exist? 70 | 71 | params = base_params.dup 72 | 73 | before_post = feed.models.last 74 | params['before'] = [before_post.published_at, before_post.version].join(' ') 75 | 76 | params 77 | end 78 | 79 | def prev_params 80 | return unless prev_posts_exist? 81 | 82 | params = base_params.dup 83 | 84 | since_post = feed.models.first 85 | params['since'] = [since_post.published_at, since_post.version].join(' ') 86 | 87 | params 88 | end 89 | 90 | def serialize_params(params) 91 | query = params.inject([]) do |memo, (key, val)| 92 | if Array === val && key.to_s == 'mentions' 93 | val.each { |v| memo.push("#{key}=#{URI.encode_www_form_component(v)}") } 94 | elsif Array === val 95 | memo.push("#{key}=#{val.map { |v| URI.encode_www_form_component(v) }.join(',') }") 96 | else 97 | memo.push("#{key}=#{URI.encode_www_form_component(val)}") 98 | end 99 | memo 100 | end.join('&') 101 | 102 | "?#{query}" 103 | end 104 | 105 | def as_json(options = {}) 106 | { 107 | :first => first_params, 108 | :last => last_params, 109 | :next => next_params, 110 | :prev => prev_params 111 | }.inject({}) { |memo, (k,v)| 112 | memo[k] = serialize_params(v) if v 113 | memo 114 | } 115 | end 116 | end 117 | 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/tentd/model.rb: -------------------------------------------------------------------------------- 1 | require 'sequel-json' 2 | require 'sequel-pg_array' 3 | require 'tentd/sequel/plugins/paranoia' 4 | 5 | module TentD 6 | module Model 7 | 8 | NoDatabaseError = Class.new(StandardError) 9 | unless TentD.database 10 | raise NoDatabaseError.new("You need to set ENV['DATABASE_URL'] or pass database_url option to TentD.setup!") 11 | end 12 | 13 | class << self 14 | attr_writer :soft_delete 15 | end 16 | 17 | def self.soft_delete 18 | @soft_delete.nil? ? true : @soft_delete 19 | end 20 | 21 | require 'tentd/models/type' 22 | require 'tentd/models/entity' 23 | require 'tentd/models/user' 24 | require 'tentd/models/parent' 25 | require 'tentd/models/post' 26 | require 'tentd/models/post_builder' 27 | require 'tentd/models/app' 28 | require 'tentd/models/app_auth' 29 | require 'tentd/models/posts_attachment' 30 | require 'tentd/models/relationship' 31 | require 'tentd/models/subscription' 32 | require 'tentd/models/credentials' 33 | require 'tentd/models/mention' 34 | require 'tentd/models/delivery_failure' 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tentd/models/app.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class App < Sequel::Model(TentD.database[:apps]) 5 | plugin :serialization 6 | serialize_attributes :pg_array, :read_types, :read_type_ids, :write_types, :scopes, :notification_type_base_ids, :notification_type_ids 7 | 8 | plugin :paranoia if Model.soft_delete 9 | 10 | def self.find_by_client_id(current_user, client_id) 11 | qualify.join(:posts, :posts__id => :apps__post_id).where(:posts__user_id => current_user.id, :posts__public_id => client_id).first 12 | end 13 | 14 | def self.subscriber_query(post, options = {}) 15 | q = Query.new(App) 16 | 17 | if columns = options.delete(:select) 18 | q.select_columns = Array(columns) 19 | end 20 | 21 | q.query_conditions << "user_id = ?" 22 | q.query_bindings << post.user_id 23 | 24 | q.query_conditions << "notification_url IS NOT NULL" 25 | q.query_conditions << "notification_type_ids IS NOT NULL" 26 | q.query_conditions << "read_type_ids IS NOT NULL" 27 | 28 | q.query_conditions << "credentials_post_id IS NOT NULL" 29 | 30 | q.query_conditions << ["AND", 31 | ["OR", 32 | "(?)::text = ANY (notification_type_ids)", 33 | "(?)::text = ANY (notification_type_ids)", 34 | "(?)::text = ANY (notification_type_ids)" 35 | ], 36 | ["OR", 37 | "(?)::text = ANY (read_type_ids)", 38 | "(?)::text = ANY (read_type_ids)", 39 | "(?)::text = ANY (read_type_ids)" 40 | ] 41 | ] 42 | 43 | all_type_id = Type.find_or_create_full('all').id 44 | 45 | # notification_type_ids 46 | q.query_bindings << post.type_id 47 | q.query_bindings << post.type_base_id 48 | q.query_bindings << all_type_id 49 | 50 | # read_type_ids 51 | q.query_bindings << post.type_id 52 | q.query_bindings << post.type_base_id 53 | q.query_bindings << all_type_id 54 | 55 | q 56 | end 57 | 58 | def self.subscribers?(post) 59 | subscriber_query(post).any? 60 | end 61 | 62 | def self.subscribers(post, options = {}) 63 | subscriber_query(post, options).all 64 | end 65 | 66 | def self.update_or_create_from_post(post, options = {}) 67 | attrs = { 68 | :notification_url => post.content['notification_url'], 69 | } 70 | 71 | if post.content['notification_types'].to_a.any? 72 | types = Type.find_or_create_types(post.content['notification_types']) 73 | attrs[:notification_type_ids] = types.map(&:id).uniq 74 | end 75 | 76 | if app = qualify.join(:posts, :posts__id => :apps__post_id).where(:apps__user_id => post.user_id, :posts__public_id => post.public_id).first 77 | app.update(attrs) if attrs.any? do |k,v| 78 | app.send(k) != v 79 | end 80 | 81 | app 82 | else 83 | if options[:create_credentials] 84 | credentials_post = Model::Credentials.generate(User.first(:id => post.user_id), post, :bidirectional_mention => true) 85 | 86 | attrs[:credentials_post_id] = credentials_post.id 87 | attrs[:hawk_key] = credentials_post.content['hawk_key'] 88 | end 89 | 90 | create(attrs.merge( 91 | :user_id => post.user_id, 92 | :post_id => post.id 93 | )) 94 | end 95 | end 96 | 97 | def self.update_app_auth(auth_post, public_id) 98 | return unless public_id 99 | 100 | app = qualify.join(:posts, :posts__id => :apps__post_id).where( 101 | :posts__user_id => auth_post.user_id, 102 | :posts__public_id => public_id 103 | ).first 104 | return unless app 105 | 106 | app.update( 107 | :auth_post_id => auth_post.id, 108 | :read_types => auth_post.content['types']['read'].to_a, 109 | :read_type_ids => Type.find_types(auth_post.content['types']['read'].to_a).map(&:id).uniq, 110 | :write_types => auth_post.content['types']['write'].to_a 111 | ) 112 | end 113 | 114 | def self.update_credentials(credentials_post, public_id) 115 | return unless public_id 116 | 117 | app = qualify.join(:posts, :posts__id => :apps__post_id).where( 118 | :posts__user_id => credentials_post.user_id, 119 | :posts__public_id => public_id 120 | ).first 121 | return unless app 122 | 123 | app.update(:credentials_post_id => credentials_post.id, :hawk_key => credentials_post.content['hawk_key']) 124 | end 125 | 126 | def self.update_app_auth_credentials(credentials_post, public_id) 127 | return unless public_id 128 | 129 | app = qualify.join(:posts, :posts__id => :apps__auth_post_id).where( 130 | :posts__user_id => credentials_post.user_id, 131 | :posts__public_id => public_id 132 | ).first 133 | return unless app 134 | 135 | app.update( 136 | :auth_hawk_key => credentials_post.content['hawk_key'], 137 | :auth_credentials_post_id => credentials_post.id, 138 | ) 139 | end 140 | 141 | def post 142 | Model::Post.first(:id => self.post_id) 143 | end 144 | end 145 | 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/tentd/models/app_auth.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class AppAuth 5 | def self.create_from_env(env) 6 | data = env['data'] 7 | 8 | app_mention = data['mentions'].to_a.find { |m| TentType.new(m['type']).base == %(https://tent.io/types/app) } 9 | app_public_id = app_mention['post'] if app_mention 10 | 11 | unless app_public_id 12 | raise Post::CreateFailure.new("Post must mention an app") 13 | end 14 | 15 | app_post = Post.where( 16 | :user_id => env['current_user'].id, 17 | :public_id => app_public_id 18 | ).order(Sequel.desc(:received_at)).first 19 | 20 | unless app_public_id 21 | raise Post::CreateFailure.new("Post must mention an existing app") 22 | end 23 | 24 | self.create( 25 | env['current_user'], 26 | app_post, 27 | data['content']['types'], 28 | data['content']['scopes'].to_a 29 | ) 30 | end 31 | 32 | def self.create(current_user, app_post, types, scopes = []) 33 | 34 | type, base_type = Type.find_or_create("https://tent.io/types/app-auth/v0#") 35 | published_at_timestamp = Utils.timestamp 36 | 37 | post_attrs = { 38 | :user_id => current_user.id, 39 | :entity_id => current_user.entity_id, 40 | :entity => current_user.entity, 41 | 42 | :type => type.type, 43 | :type_id => type.id, 44 | :type_base_id => base_type.id, 45 | 46 | :version_published_at => published_at_timestamp, 47 | :version_received_at => published_at_timestamp, 48 | :published_at => published_at_timestamp, 49 | :received_at => published_at_timestamp, 50 | 51 | :content => { 52 | :types => types, 53 | :scopes => scopes 54 | }, 55 | 56 | :mentions => [ 57 | { "entity" => current_user.entity, "type" => app_post.type, "post" => app_post.public_id, "version" => app_post.version } 58 | ] 59 | } 60 | 61 | post = Post.create(post_attrs) 62 | credentials_post = Credentials.generate(current_user, post, :bidirectional_mention => true) 63 | 64 | # Ref credentials and app 65 | post.refs = [ 66 | { "entity" => current_user.entity, "type" => app_post.type, "post" => app_post.public_id, "version" => app_post.version }, 67 | { "entity" => current_user.entity, "type" => credentials_post.type, "post" => credentials_post.public_id } 68 | ] 69 | post = post.save_version(:public_id => post.public_id) 70 | 71 | # Update app record 72 | unless app = App.first(:post_id => app_post.id) 73 | # app doesn't exist 74 | app = App.update_or_create_from_post(app_post) 75 | end 76 | app.update( 77 | :auth_post_id => post.id, 78 | :auth_hawk_key => credentials_post.content['hawk_key'], 79 | :auth_credentials_post_id => credentials_post.id, 80 | 81 | :read_types => types['read'], 82 | :read_type_ids => Type.find_types(types['read']).map(&:id), 83 | :write_types => types['write'] 84 | ) 85 | 86 | Mention.create( 87 | :user_id => current_user.id, 88 | :post_id => post.id, 89 | :entity_id => post.entity_id, 90 | :entity => post.entity, 91 | :type_id => app_post.type_id, 92 | :type => app_post.type, 93 | :post => app_post.public_id 94 | ) 95 | 96 | Mention.link_posts(app_post, post) 97 | 98 | post 99 | end 100 | 101 | def self.update_app_post_refs(post, app_post) 102 | # Update app post to ref auth 103 | app_post.refs ||= [] 104 | app_post.refs.delete_if { |ref| ref['type'] == post.type } 105 | app_post.refs.push( 106 | 'type' => post.type, 107 | 'entity' => post.entity, 108 | 'post' => post.public_id 109 | ) 110 | app_post.save 111 | app_post 112 | end 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/tentd/models/attachment/fog.rb: -------------------------------------------------------------------------------- 1 | require 'fog' 2 | 3 | module TentD 4 | module Model 5 | 6 | class Attachment 7 | 8 | class << self 9 | attr_accessor :fog_adapter, :namespace 10 | end 11 | 12 | def self.connection 13 | @connection ||= Fog::Storage.new(fog_adapter) 14 | end 15 | 16 | def self.directory 17 | @directory ||= begin 18 | connection.directories.get(namespace) || connection.directories.create(:key => namespace) 19 | end 20 | end 21 | 22 | def self.find_by_digest(digest) 23 | find(:digest => digest) 24 | end 25 | 26 | def self.find(attrs) 27 | if file = directory.files.head(attrs[:digest]) 28 | new(file) 29 | else 30 | nil 31 | end 32 | end 33 | 34 | def self.create(attrs) 35 | new(directory.files.create( 36 | :body => attrs[:data], 37 | :key => attrs[:digest] 38 | )) 39 | end 40 | 41 | def self.find_or_create(attrs) 42 | find(attrs) || create(attrs) 43 | end 44 | 45 | def initialize(file) 46 | @file = file 47 | end 48 | 49 | def id 50 | nil # for posts_attachments record 51 | end 52 | 53 | def digest 54 | @file.key 55 | end 56 | 57 | def size 58 | @file.content_length 59 | end 60 | 61 | def data(&block) 62 | @file.collection.get(@file.key, &block) 63 | end 64 | alias each data 65 | 66 | def read 67 | data.body 68 | end 69 | 70 | end 71 | 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/tentd/models/attachment/sequel.rb: -------------------------------------------------------------------------------- 1 | require 'tentd/models/large_object' 2 | 3 | module TentD 4 | module Model 5 | 6 | class Attachment < Sequel::Model(TentD.database[:attachments]) 7 | include SequelPGLargeObject 8 | 9 | pg_large_object :data 10 | 11 | def self.find_by_digest(digest) 12 | where(:digest => digest).first 13 | end 14 | 15 | def self.find_or_create(attrs) 16 | if String === attrs[:data] 17 | attrs[:data] = StringIO.new(attrs[:data]) 18 | end 19 | 20 | create(attrs) 21 | rescue Sequel::UniqueConstraintViolation => e 22 | if e.message =~ /duplicate key.*unique_attachments/ 23 | first(:digest => attrs[:digest], :size => attrs[:size]) 24 | else 25 | raise 26 | end 27 | end 28 | 29 | def each(&block) 30 | return unless self.data 31 | self.data.each(&block) 32 | end 33 | 34 | def read 35 | return unless self.data 36 | self.data.read 37 | end 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/tentd/models/credentials.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Credentials 5 | def self.generate(current_user, target_post=nil, options = {}) 6 | if target_post 7 | target_post_type = TentType.new(target_post.type) 8 | type, base_type = Type.find_or_create("https://tent.io/types/credentials/v0##{target_post_type.to_s(:fragment => false)}") 9 | else 10 | type, base_type = Type.find_or_create("https://tent.io/types/credentials/v0#") 11 | end 12 | published_at_timestamp = (Time.now.to_f * 1000).to_i 13 | 14 | post_attrs = { 15 | :user_id => current_user.id, 16 | :entity_id => current_user.entity_id, 17 | :entity => current_user.entity, 18 | 19 | :type => type.type, 20 | :type_id => type.id, 21 | :type_base_id => base_type.id, 22 | 23 | :version_published_at => published_at_timestamp, 24 | :version_received_at => published_at_timestamp, 25 | :published_at => published_at_timestamp, 26 | :received_at => published_at_timestamp, 27 | 28 | :content => { 29 | :hawk_key => TentD::Utils.hawk_key, 30 | :hawk_algorithm => TentD::Utils.hawk_algorithm 31 | }, 32 | } 33 | 34 | if target_post 35 | post_attrs[:mentions] = [ 36 | { "entity" => current_user.entity, "type" => target_post.type, "post" => target_post.public_id } 37 | ] 38 | end 39 | 40 | post = Post.create(post_attrs) 41 | 42 | if target_post 43 | Mention.create( 44 | :user_id => current_user.id, 45 | :post_id => post.id, 46 | :entity_id => post.entity_id, 47 | :entity => post.entity, 48 | :type_id => target_post.type_id, 49 | :type => target_post.type, 50 | :post => target_post.public_id 51 | ) 52 | 53 | if options[:bidirectional_mention] 54 | Mention.link_posts(target_post, post) 55 | end 56 | end 57 | 58 | post 59 | end 60 | 61 | def self.refresh_key(credentials_post) 62 | credentials_post.content["hawk_key"] = Utils.hawk_key 63 | credentials_post.save_version # => new credentials_post 64 | end 65 | 66 | def self.slice_credentials(credentials_post) 67 | TentD::Utils::Hash.symbolize_keys(credentials_post.as_json[:content]).merge(:id => credentials_post.public_id) 68 | end 69 | 70 | def self.lookup(current_user, public_id) 71 | Post.first( 72 | :user_id => current_user.id, 73 | :public_id => public_id, 74 | :type_base_id => Type.find_or_create_base("https://tent.io/types/credentials/v0#").id 75 | ) 76 | end 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/tentd/models/delivery_failure.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class DeliveryFailure < Sequel::Model(TentD.database[:delivery_failures]) 5 | def self.find_or_create(entity, post, status, reason) 6 | delivery_failure = create( 7 | :user_id => post.user_id, 8 | :failed_post_id => post.id, 9 | :entity => entity, 10 | :status => status, 11 | :reason => reason 12 | ) 13 | 14 | ref = { 15 | 'entity' => post.entity, 16 | 'post' => post.public_id, 17 | 'version' => post.version, 18 | 'type' => post.type 19 | } 20 | 21 | type = TentType.new("https://tent.io/types/delivery-failure/v0#") 22 | type.fragment = TentType.new(post.type).to_s(:fragment => false) 23 | 24 | post = PostBuilder.create_from_env( 25 | 'current_user' => post.user, 26 | 'data' => { 27 | 'type' => type.to_s, 28 | 'refs' => [ ref ], 29 | 'content' => { 30 | 'entity' => entity, 31 | 'status' => status, 32 | 'reason' => reason 33 | } 34 | } 35 | ) 36 | 37 | delivery_failure.update(:post_id => post.id) 38 | delivery_failure 39 | rescue Sequel::UniqueConstraintViolation => e 40 | where( 41 | :user_id => post.user_id, 42 | :failed_post_id => post.id, 43 | :entity => entity, 44 | :status => status 45 | ).first 46 | end 47 | end 48 | 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/tentd/models/entity.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Entity < Sequel::Model(TentD.database[:entities]) 5 | def self.first_or_create(entity_uri) 6 | first(:entity => entity_uri) || create(:entity => entity_uri) 7 | rescue Sequel::UniqueConstraintViolation 8 | first(:entity => entity_uri) 9 | end 10 | 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/tentd/models/large_object.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class PGLargeObject 5 | CHUNK_SIZE = 1_000_000.freeze # 1 MB 6 | 7 | def self.find(connection_pool, oid) 8 | object = new(connection_pool, oid) 9 | return nil unless object.exists? 10 | object 11 | end 12 | 13 | def self.create(connection_pool, io) 14 | object = new(connection_pool) 15 | object.create(io) 16 | object 17 | end 18 | 19 | attr_accessor :oid 20 | def initialize(connection_pool, oid=nil) 21 | @connection_pool, @oid = connection_pool, oid 22 | end 23 | 24 | def connection 25 | @connection_pool.available_connections.first 26 | end 27 | 28 | def exists? 29 | return unless @oid 30 | connection.transaction do 31 | descriptor = connection.lo_open(@oid, PG::INV_READ) 32 | connection.lo_close(descriptor) 33 | end 34 | true 35 | rescue PG::Error 36 | end 37 | 38 | def each(&block) 39 | return unless @oid 40 | connection.transaction do 41 | descriptor = connection.lo_open(@oid, PG::INV_READ) 42 | while chunk = connection.lo_read(descriptor, CHUNK_SIZE) 43 | yield(chunk) 44 | end 45 | connection.lo_close(descriptor) 46 | end 47 | end 48 | 49 | def read 50 | return unless @oid 51 | data = "" 52 | each { |chunk| data << chunk } 53 | data 54 | end 55 | 56 | def create(io) 57 | connection.transaction do 58 | @oid = connection.lo_creat(PG::INV_WRITE) 59 | descriptor = connection.lo_open(oid, PG::INV_WRITE) 60 | while chunk = io.read(CHUNK_SIZE) 61 | connection.lo_write(descriptor, chunk) 62 | end 63 | io.rewind if io.respond_to?(:rewind) 64 | connection.lo_close(descriptor) 65 | end 66 | @oid 67 | end 68 | 69 | def destroy 70 | connection.transaction do 71 | connection.lo_unlink(@oid) 72 | end 73 | true 74 | rescue PG::Error 75 | end 76 | end 77 | 78 | module SequelPGLargeObject 79 | def self.included(model) 80 | model.extend(ClassMethods) 81 | end 82 | 83 | module ClassMethods 84 | # each name should have an integer column of the same name with "_oid" suffix 85 | def pg_large_object(*names) 86 | names.each do |name| 87 | define_method name do 88 | oid = self[:"#{name}_oid"] 89 | 90 | Model::PGLargeObject.find(self.db.pool, oid) 91 | end 92 | 93 | define_method "#{name}=" do |io| 94 | object = Model::PGLargeObject.create(self.db.pool, io) 95 | self["#{name}_oid"] = object.oid 96 | object 97 | end 98 | end 99 | end 100 | end 101 | end 102 | 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/tentd/models/mention.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Mention < Sequel::Model(TentD.database[:mentions]) 5 | 6 | def self.create(attrs) 7 | super 8 | rescue Sequel::UniqueConstraintViolation => e 9 | where(:user_id => attrs[:user_id], :entity_id => attrs[:entity_id], :post => attrs[:post]).first 10 | end 11 | 12 | def self.link_posts(source_post, target_post, options = {}) 13 | source_post.mentions ||= [] 14 | source_post.mentions << { "entity" => target_post.entity, "type" => target_post.type, "post" => target_post.public_id } 15 | if options[:save_version] 16 | source_post.save_version 17 | else 18 | source_post.version = TentD::Utils.hex_digest(source_post.canonical_json) 19 | source_post.save 20 | end 21 | 22 | create( 23 | :user_id => source_post.user_id, 24 | :post_id => source_post.id, 25 | :entity_id => target_post.entity_id, 26 | :entity => target_post.entity, 27 | :type_id => target_post.type_id, 28 | :type => target_post.type, 29 | :post => target_post.public_id 30 | ) 31 | 32 | true 33 | end 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/tentd/models/parent.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Parent < Sequel::Model(TentD.database[:parents]) 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tentd/models/post.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'openssl' 3 | require 'tent-canonical-json' 4 | 5 | module TentD 6 | module Model 7 | 8 | class Post < Sequel::Model(TentD.database[:posts]) 9 | CreateFailure = Class.new(StandardError) 10 | 11 | plugin :serialization 12 | serialize_attributes :pg_array, :permissions_entities, :permissions_groups 13 | serialize_attributes :json, :mentions, :refs, :attachments, :version_parents, :licenses, :content 14 | 15 | plugin :paranoia if Model.soft_delete 16 | 17 | attr_writer :user 18 | 19 | def before_create 20 | self.public_id ||= TentD::Utils.random_id 21 | self.version = TentD::Utils.hex_digest(canonical_json) 22 | self.received_at ||= TentD::Utils.timestamp 23 | self.version_received_at ||= TentD::Utils.timestamp 24 | end 25 | 26 | def save_version(options = {}) 27 | data = as_json 28 | data[:version] = { 29 | :parents => [ 30 | { :version => data[:version][:id], :post => data[:id] } 31 | ] 32 | } 33 | 34 | env = { 35 | 'data' => TentD::Utils::Hash.stringify_keys(data), 36 | 'current_user' => User.first(:id => user_id) 37 | } 38 | 39 | self.class.create_from_env(env, options) 40 | end 41 | 42 | def latest_version 43 | self.class.where(:public_id => public_id).order(Sequel.desc(:version_published_at)).first 44 | end 45 | 46 | def around_save 47 | if (self.changed_columns & [:public]).any? 48 | if super 49 | queue_delivery 50 | end 51 | else 52 | super 53 | end 54 | end 55 | 56 | def after_destroy 57 | if TentType.new(self.type).base == %(https://tent.io/types/subscription) 58 | Subscription.post_destroyed(self) 59 | end 60 | 61 | super 62 | end 63 | 64 | def queue_delivery 65 | return unless deliverable? 66 | Worker::NotificationDispatch.perform_async(self.id) 67 | end 68 | 69 | # Determines if notifications should be sent out 70 | def deliverable? 71 | return false unless self.entity_id == User.select(:entity_id).where(:id => self.user_id).first.entity_id 72 | self.public || self.permissions_entities.to_a.any? || self.permissions_groups.to_a.any? || App.subscribers?(self) 73 | end 74 | 75 | class << self 76 | alias _create create 77 | def create(attrs) 78 | _create(attrs) 79 | rescue Sequel::UniqueConstraintViolation => e 80 | params = { 81 | :user_id => attrs[:user_id], 82 | :entity_id => attrs[:entity_id], 83 | :public_id => attrs[:public_id], 84 | :version => attrs[:version] 85 | } 86 | 87 | TentD.logger.debug "Post.create: UniqueConstraintViolation" if TentD.settings[:debug] 88 | TentD.logger.debug "Post.create -> Post.first(#{params.inspect})" if TentD.settings[:debug] 89 | 90 | post = first(params) 91 | 92 | TentD.logger.debug "Post.first => Post(#{post ? post.id : nil.inspect})" if TentD.settings[:debug] 93 | 94 | raise CreateFailure.new("Server Error: #{Yajl::Encoder.encode(params)}") unless post 95 | 96 | post 97 | end 98 | end 99 | 100 | def self.create_from_env(env, options = {}) 101 | PostBuilder.create_from_env(env, options) 102 | end 103 | 104 | def self.create_version_from_env(env, options = {}) 105 | TentD.logger.debug "Post.create_version_from_env" if TentD.settings[:debug] 106 | 107 | PostBuilder.create_from_env(env, options.merge(:public_id => env['params']['post'])) 108 | end 109 | 110 | def self.import_notification(env) 111 | TentD.logger.debug "Post.import_notification" if TentD.settings[:debug] 112 | 113 | create_version_from_env(env, :notification => true, :entity => env['params']['entity']) 114 | end 115 | 116 | def destroy(options = {}) 117 | _id = self.id 118 | 119 | _res = if options.delete(:create_delete_post) 120 | if super(options) 121 | if options[:delete_version] 122 | PostBuilder.create_delete_post(self, :version => true) 123 | else 124 | PostBuilder.create_delete_post(self) 125 | end 126 | else 127 | false 128 | end 129 | else 130 | super(options) 131 | end 132 | 133 | if _res && !options[:delete_version] 134 | # delete all parents and children 135 | children = Post.qualify.join(:parents, :parents__post_id => :posts__id).where(:parents__parent_post_id => _id).all.to_a 136 | parents = Post.qualify.join(:parents, :parents__parent_post_id => :posts__id).where(:parents__post_id => _id).all.to_a 137 | Parent.where(Sequel.|(:post_id => _id, :parent_post_id => _id)).destroy 138 | children.each { |child| child.destroy(options) } 139 | parents.each { |child| child.destroy(options) } 140 | end 141 | 142 | _res 143 | end 144 | 145 | def user 146 | @user ||= User.where(:id => self.user_id).first 147 | end 148 | 149 | def create_attachments(attachments) 150 | PostBuilder.create_attachments(self, attachments) 151 | end 152 | 153 | def create_mentions(mentions) 154 | PostBuilder.create_mentions(self, mentions) 155 | end 156 | 157 | def create_version_parents(version_parents) 158 | PostBuilder.create_version_parents(self, version_parents) 159 | end 160 | 161 | def version_as_json(options = {}) 162 | obj = { 163 | :id => self.version, 164 | :parents => self.version_parents, 165 | :message => self.version_message, 166 | :published_at => self.version_published_at, 167 | :received_at => self.version_received_at 168 | } 169 | obj.delete(:parents) if obj[:parents].nil? 170 | obj.delete(:message) if obj[:message].nil? 171 | obj.delete(:received_at) if obj[:received_at].nil? 172 | 173 | if obj[:parents] && !options[:delivery] 174 | obj[:parents].each do |parent| 175 | parent.delete('post') if parent['post'] == self.public_id 176 | end 177 | end 178 | 179 | unless (env = options[:env]) && Authorizer.new(env).app? 180 | obj.delete(:received_at) 181 | end 182 | 183 | obj 184 | end 185 | 186 | def app_as_json(options = {}) 187 | obj = { 188 | :name => self.app_name, 189 | :url => self.app_url 190 | } 191 | 192 | if (env = options[:env]) && Authorizer.new(env).app? 193 | obj[:id] = self.app_id 194 | end 195 | 196 | obj.reject! { |k,v| v.nil? } 197 | 198 | obj 199 | end 200 | 201 | def as_json(options = {}) 202 | attrs = { 203 | :id => self.public_id, 204 | :type => self.type, 205 | :entity => self.entity, 206 | :published_at => self.published_at, 207 | :received_at => self.received_at, 208 | :content => self.content, 209 | :mentions => self.mentions, 210 | :refs => self.refs, 211 | :version => version_as_json(options), 212 | :app => app_as_json(options) 213 | } 214 | attrs.delete(:received_at) if attrs[:received_at].nil? 215 | attrs.delete(:content) if attrs[:content].nil? 216 | attrs.delete(:mentions) if attrs[:mentions].nil? 217 | attrs.delete(:refs) if attrs[:refs].nil? 218 | attrs.delete(:app) if attrs[:app].keys.empty? 219 | 220 | if self.original_entity 221 | attrs[:original_entity] = self.original_entity 222 | end 223 | 224 | unless (env = options[:env]) && Authorizer.new(env).app? 225 | attrs.delete(:received_at) 226 | end 227 | 228 | if Array(self.attachments).any? 229 | attrs[:attachments] = self.attachments 230 | end 231 | 232 | if attrs[:mentions] 233 | attrs[:mentions].each do |m| 234 | m.delete('entity') if m['entity'] == self.entity 235 | end 236 | end 237 | 238 | if attrs[:refs] 239 | attrs[:refs].each do |m| 240 | m.delete('entity') if m['entity'] == self.entity 241 | end 242 | end 243 | 244 | if self[:public] == false 245 | attrs[:permissions] = { :public => false } 246 | if (env = options[:env]) && Authorizer.new(env).app? 247 | attrs[:permissions][:entities] = self.permissions_entities if self.permissions_entities.to_a.any? 248 | end 249 | end 250 | 251 | attrs 252 | end 253 | 254 | def canonical_json 255 | TentCanonicalJson.encode(as_json) 256 | end 257 | 258 | end 259 | 260 | end 261 | end 262 | -------------------------------------------------------------------------------- /lib/tentd/models/posts_attachment.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class PostsAttachment < Sequel::Model(TentD.database[:posts_attachments]) 5 | def self.create(attrs) 6 | super 7 | rescue Sequel::UniqueConstraintViolation => e 8 | if e.message =~ /duplicate key.*unique_posts_attachments/ 9 | attrs = Utils::Hash.symbolize_keys(attrs) 10 | first(:post_id => attrs[:post_id], :attachment_id => attrs[:attachment_id], :content_type => attrs[:content_type]) 11 | else 12 | raise 13 | end 14 | end 15 | end 16 | 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/tentd/models/refs.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Ref < Sequel::Model(TentD.database[:refs]) 5 | end 6 | 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/tentd/models/relationship.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Relationship < Sequel::Model(TentD.database[:relationships]) 5 | plugin :serialization 6 | serialize_attributes :json, :remote_credentials 7 | 8 | plugin :paranoia if Model.soft_delete 9 | 10 | attr_writer :post, :credentials_post, :meta_post 11 | 12 | def before_save 13 | if self.remote_credentials_id 14 | self.active = true 15 | end 16 | 17 | super 18 | end 19 | 20 | def self.create_initial(current_user, target_entity, relationship = nil) 21 | type, base_type = Type.find_or_create("https://tent.io/types/relationship/v0#initial") 22 | published_at_timestamp = TentD::Utils.timestamp 23 | 24 | attrs = { 25 | :user_id => current_user.id, 26 | :entity_id => current_user.entity_id, 27 | :entity => current_user.entity, 28 | 29 | :type => type.type, 30 | :type_id => type.id, 31 | :type_base_id => base_type.id, 32 | 33 | :version_published_at => published_at_timestamp, 34 | :version_received_at => published_at_timestamp, 35 | :published_at => published_at_timestamp, 36 | :received_at => published_at_timestamp, 37 | 38 | :mentions => [ 39 | { "entity" => target_entity } 40 | ] 41 | } 42 | 43 | post = Post.create(attrs) 44 | post.create_mentions(attrs[:mentions]) 45 | 46 | credentials_post = Model::Credentials.generate(current_user, post) 47 | 48 | relationship_attrs = { 49 | :user_id => current_user.id, 50 | :entity_id => Entity.first_or_create(target_entity).id, 51 | :entity => target_entity, 52 | :post_id => post.id, 53 | :type_id => post.type_id, 54 | :credentials_post_id => credentials_post.id, 55 | } 56 | 57 | if relationship 58 | relationship.update(relationship_attrs) 59 | else 60 | relationship = create(relationship_attrs) 61 | end 62 | 63 | relationship.post = post 64 | relationship.credentials_post = credentials_post 65 | 66 | relationship 67 | end 68 | 69 | def self.create_final(current_user, parts = {}) 70 | remote_relationship = parts.delete(:remote_relationship) 71 | remote_credentials = parts.delete(:remote_credentials) 72 | remote_meta_post = parts.delete(:remote_meta_post) 73 | remote_entity = remote_relationship[:entity] 74 | 75 | type, base_type = Type.find_or_create("https://tent.io/types/relationship/v0#") 76 | published_at_timestamp = Utils.timestamp 77 | 78 | attrs = { 79 | :user_id => current_user.id, 80 | :entity_id => current_user.entity_id, 81 | :entity => current_user.entity, 82 | 83 | :type => type.type, 84 | :type_id => type.id, 85 | :type_base_id => base_type.id, 86 | 87 | :version_published_at => published_at_timestamp, 88 | :published_at => published_at_timestamp, 89 | :received_at => published_at_timestamp, 90 | 91 | :permissions_entities => [remote_entity] 92 | } 93 | 94 | attrs[:mentions] = [{ 95 | 'entity' => remote_relationship[:entity], 96 | 'type' => remote_relationship[:type], 97 | 'post' => remote_relationship[:id] 98 | }] 99 | 100 | post = Post.create(attrs) 101 | post.create_mentions(attrs[:mentions]) 102 | 103 | credentials_post = Model::Credentials.generate(current_user, post) 104 | 105 | remote_entity_id = Entity.first_or_create(remote_entity).id 106 | 107 | relationship = create( 108 | :user_id => current_user.id, 109 | :entity_id => remote_entity_id, 110 | :entity => remote_entity, 111 | :post_id => post.id, 112 | :type_id => post.type_id, 113 | :meta_post_id => remote_meta_post.id, 114 | :credentials_post_id => credentials_post.id, 115 | 116 | :remote_credentials_id => remote_credentials[:id], # for easy lookup 117 | :remote_credentials => { 118 | 'id' => remote_credentials[:id], 119 | 'hawk_key' => remote_credentials[:content][:hawk_key], 120 | 'hawk_algorithm' => remote_credentials[:content][:hawk_algorithm] 121 | } 122 | ) 123 | 124 | relationship.post = post 125 | relationship.credentials_post = credentials_post 126 | relationship.meta_post = remote_meta_post 127 | 128 | post.queue_delivery 129 | 130 | relationship 131 | end 132 | 133 | def self.update_meta_post_ids(meta_post) 134 | where(:user_id => meta_post.user_id, :entity_id => meta_post.entity_id).update(:meta_post_id => meta_post.id) 135 | end 136 | 137 | def post 138 | @post ||= Post.where(:id => self.post_id).first 139 | end 140 | 141 | def credentials_post 142 | @credentials_post ||= Post.where(:id => self.credentials_post_id).first 143 | end 144 | 145 | def meta_post 146 | @meta_post ||= Post.where(:id => self.meta_post_id).first 147 | end 148 | 149 | def client(options = {}) 150 | if self.meta_post 151 | options.merge!(:server_meta => Utils::Hash.stringify_keys(self.meta_post.as_json)) 152 | end 153 | 154 | TentClient.new(self.entity, options.merge( 155 | :credentials => Utils::Hash.symbolize_keys(self.remote_credentials) 156 | )) 157 | end 158 | 159 | def finalize 160 | type, base_type = Type.find_or_create("https://tent.io/types/relationship/v0#") 161 | 162 | post.type = type.type 163 | post.type_id = type.id 164 | post.type_base_id = base_type.id 165 | 166 | @post = post.save_version(:public_id => post.public_id) 167 | 168 | self.post_id = post.id 169 | self.type_id = type.id 170 | 171 | save 172 | end 173 | 174 | def set_public 175 | post.update(:public => true) 176 | end 177 | end 178 | 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/tentd/models/subscription.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Subscription < Sequel::Model(TentD.database[:subscriptions]) 5 | plugin :paranoia if Model.soft_delete 6 | 7 | attr_writer :post 8 | attr_accessor :deliver 9 | def self.find_or_create(post_attrs) 10 | TentD.logger.debug "Subscription.find_or_create" if TentD.settings[:debug] 11 | 12 | unless target_entity = (post_attrs[:mentions].to_a.first || {})['entity'] 13 | TentD.logger.debug "Subscription.find_or_create: Must mention an entity" if TentD.settings[:debug] 14 | 15 | raise Post::CreateFailure.new("Subscription must mention an entity! #{post_attrs[:mentions].inspect}") 16 | end 17 | 18 | type = Type.find_or_create_full(post_attrs[:content]['type']) 19 | target_entity_id = Entity.first_or_create(target_entity).id 20 | unless subscription = where(:user_id => post_attrs[:user_id], :type_id => type.id, :entity_id => target_entity_id, :subscriber_entity_id => post_attrs[:entity_id]).first 21 | TentD.logger.debug "Subscription.find_or_create -> Post.create" if TentD.settings[:debug] 22 | 23 | post = Post.create(post_attrs) 24 | 25 | TentD.logger.debug "Subscription.find_or_create -> Subscription.create" if TentD.settings[:debug] 26 | 27 | subscription = create( 28 | :user_id => post.user_id, 29 | :post_id => post.id, 30 | :subscriber_entity_id => post.entity_id, 31 | :subscriber_entity => post.entity, 32 | :entity_id => target_entity_id, 33 | :entity => target_entity, 34 | :type_id => (type ? type.id : nil), # nil if 'all' 35 | :type => post.content['type'] 36 | ) 37 | 38 | existing_relationship = Relationship.where(:user_id => post.user_id, :entity_id => target_entity_id).first 39 | unless existing_relationship 40 | TentD.logger.debug "Subscription.find_or_create -> RelationshipInitiation.perform_async" if TentD.settings[:debug] 41 | 42 | subscription.deliver = false 43 | Worker::RelationshipInitiation.perform_async(post.user_id, target_entity_id, subscription.post_id) 44 | end 45 | 46 | subscription.post = post 47 | end 48 | 49 | subscription 50 | end 51 | 52 | def self.create_from_notification(current_user, post_attrs, relationship_post) 53 | TentD.logger.debug "Subscription.create_from_notification" if TentD.settings[:debug] 54 | 55 | unless target_entity = (post_attrs[:mentions].to_a.first || {})['entity'] 56 | TentD.logger.debug "Subscription.create_from_notification: Must mention an entity" if TentD.settings[:debug] 57 | 58 | raise Post::CreateFailure.new("Subscription must mention an entity! #{post_attrs[:mentions].inspect}") 59 | end 60 | 61 | unless subscription = where(:user_id => current_user.id, :type => post_attrs[:content]['type'], :entity_id => current_user.entity_id, :subscriber_entity_id => post_attrs[:entity_id]).first 62 | TentD.logger.debug "Subscription.create_from_notification -> Post.create" if TentD.settings[:debug] 63 | 64 | post = Post.create(post_attrs) 65 | 66 | if post.public 67 | TentD.logger.debug "Subscription.create_from_notification: Make relationship public for Entity(#{post.entity})" if TentD.settings[:debug] 68 | 69 | Relationship.where( 70 | :user_id => current_user.id, 71 | :entity_id => post.entity_id 72 | ).first.set_public 73 | end 74 | 75 | # Don't create a subscription record if we're not the target 76 | unless target_entity == current_user.entity 77 | TentD.logger.debug "Subscription.create_from_notification: We (#{current_user.entity}) are not the target (#{target_entity}), don't create a subscription record" if TentD.settings[:debug] 78 | 79 | subscription = new 80 | subscription.post = post 81 | return subscription 82 | end 83 | 84 | type = Type.find_or_create_full(post_attrs[:content]['type']) 85 | target_entity_id = current_user.entity_id 86 | 87 | TentD.logger.debug "Subscription.create_from_notification -> Subscription.create" if TentD.settings[:debug] 88 | 89 | subscription = create( 90 | :user_id => post.user_id, 91 | :post_id => post.id, 92 | :subscriber_entity_id => post.entity_id, 93 | :subscriber_entity => post.entity, 94 | :entity_id => target_entity_id, 95 | :entity => target_entity, 96 | :type_id => (type ? type.id : nil), # nil if 'all' 97 | :type => post.content['type'] 98 | ) 99 | 100 | subscription.post = post 101 | end 102 | 103 | subscription 104 | end 105 | 106 | def self.post_destroyed(post) 107 | where(:post_id => post.id).destroy 108 | end 109 | 110 | def post 111 | @post ||= Post.where(:id => self.post_id).first 112 | end 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/tentd/models/type.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class Type < Sequel::Model(TentD.database[:types]) 5 | def self.find_or_create(type_uri) 6 | base_type = find_or_create_base(type_uri) 7 | full_type = find_or_create_full(type_uri) 8 | 9 | [full_type, base_type] 10 | end 11 | 12 | def self.find_types(type_uris) 13 | return [] if type_uris.empty? 14 | 15 | tent_types = type_uris.map { |uri| TentType.new(uri) } 16 | tent_types_without_fragment = tent_types.select { |t| !t.has_fragment? } 17 | tent_types_with_fragment = tent_types.select { |t| t.has_fragment? } 18 | 19 | q = Query.new(Type) 20 | 21 | _conditions = ["OR"] 22 | 23 | if tent_types_without_fragment.any? 24 | _conditions << ["AND", 25 | "base IN ?", 26 | "fragment IS NULL" 27 | ] 28 | q.query_bindings << tent_types_without_fragment.map(&:base) 29 | end 30 | 31 | tent_types_with_fragment.each do |tent_type| 32 | _conditions << ["AND", 33 | "base = ?", 34 | "fragment = ?" 35 | ] 36 | q.query_bindings << tent_type.base 37 | q.query_bindings << tent_type.fragment.to_s 38 | end 39 | 40 | q.query_conditions << _conditions 41 | 42 | types = q.all.to_a 43 | end 44 | 45 | def self.find_or_create_types(type_uris) 46 | tent_types = type_uris.map { |uri| TentType.new(uri) } 47 | 48 | types = find_types(type_uris) 49 | 50 | missing_tent_types = tent_types.reject do |tent_type| 51 | types.any? { |t| tent_type == t.tent_type } 52 | end 53 | 54 | missing_tent_types.each do |tent_type| 55 | types << find_or_create_full(tent_type.to_s) 56 | end 57 | 58 | types.compact 59 | end 60 | 61 | def self.find_or_create_base(type_uri) 62 | tent_type = TentClient::TentType.new(type_uri) 63 | 64 | return unless tent_type.base 65 | 66 | unless base_type = where(:base => tent_type.base, :fragment => nil, :version => tent_type.version.to_s).first 67 | begin 68 | base_type = create( 69 | :base => tent_type.base, 70 | :version => tent_type.version.to_s, 71 | :fragment => nil 72 | ) 73 | rescue Sequel::UniqueConstraintViolation 74 | type = where(:base => tent_type.base, :fragment => nil, :version => tent_type.version.to_s).first 75 | end 76 | end 77 | 78 | base_type 79 | end 80 | 81 | def self.find_or_create_full(type_uri) 82 | tent_type = TentClient::TentType.new(type_uri) 83 | fragment = tent_type.has_fragment? ? tent_type.fragment.to_s : nil 84 | 85 | return unless tent_type.base 86 | 87 | unless type = where(:base => tent_type.base, :fragment => fragment, :version => tent_type.version.to_s).first 88 | begin 89 | type = create( 90 | :base => tent_type.base, 91 | :version => tent_type.version.to_s, 92 | :fragment => fragment 93 | ) 94 | rescue Sequel::UniqueConstraintViolation 95 | type = where(:base => tent_type.base, :fragment => fragment, :version => tent_type.version.to_s).first 96 | end 97 | end 98 | 99 | type 100 | end 101 | 102 | def tent_type 103 | t = TentType.new 104 | t.base = self.base 105 | t.version = self.version 106 | t.fragment = self.fragment.to_s unless self.fragment.nil? 107 | t 108 | end 109 | 110 | def type 111 | tent_type.to_s 112 | end 113 | 114 | end 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/tentd/models/user.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Model 3 | 4 | class User < Sequel::Model(TentD.database[:users]) 5 | plugin :serialization 6 | serialize_attributes :json, :server_credentials 7 | 8 | plugin :paranoia if Model.soft_delete 9 | 10 | def self.create(attrs, options = {}) 11 | entity = Entity.first_or_create(attrs[:entity]) 12 | user = super(attrs.merge( 13 | :entity_id => entity.id, 14 | :server_credentials => { 15 | :id => TentD::Utils.random_id, 16 | :hawk_key => TentD::Utils.hawk_key, 17 | :hawk_algorithm => TentD::Utils.hawk_algorithm 18 | } 19 | )) 20 | user.create_meta_post(options.delete(:meta_post_attrs) || {}) 21 | user 22 | end 23 | 24 | def self.first_or_create(entity_uri) 25 | first(:entity => entity_uri) || create(:entity => entity_uri) 26 | end 27 | 28 | def create_meta_post(attrs = {}) 29 | type, base_type = Type.find_or_create("https://tent.io/types/meta/v0#") 30 | published_at_timestamp = Utils.timestamp 31 | 32 | api_root = ENV['API_ROOT'] || self.entity 33 | 34 | Utils::Hash.deep_merge!(attrs, { 35 | :user_id => self.id, 36 | :entity_id => self.entity_id, 37 | :entity => self.entity, 38 | 39 | :type => type.type, 40 | :type_id => type.id, 41 | :type_base_id => base_type.id, 42 | 43 | :version_published_at => published_at_timestamp, 44 | :version_received_at => published_at_timestamp, 45 | :published_at => published_at_timestamp, 46 | :received_at => published_at_timestamp, 47 | 48 | :content => { 49 | "entity" => self.entity, 50 | "servers" => [ 51 | { 52 | "version" => "0.3", 53 | "urls" => { 54 | "oauth_auth" => "#{api_root}/oauth/authorize", 55 | "oauth_token" => "#{api_root}/oauth/token", 56 | "posts_feed" => "#{api_root}/posts", 57 | "new_post" => "#{api_root}/posts", 58 | "post" => "#{api_root}/posts/{entity}/{post}", 59 | "post_attachment" => "#{api_root}/posts/{entity}/{post}/attachments/{name}", 60 | "attachment" => "#{api_root}/attachments/{entity}/{digest}", 61 | "batch" => "#{api_root}/batch", 62 | "server_info" => "#{api_root}/server", 63 | "discover" => "#{api_root}/discover?entity={entity}" 64 | }, 65 | "preference" => 0 66 | } 67 | ] 68 | }, 69 | :public => true 70 | }) 71 | 72 | meta_post = Post.create(attrs) 73 | 74 | self.update(:meta_post_id => meta_post.id) 75 | meta_post 76 | end 77 | 78 | def update_meta_post_id(meta_post) 79 | return unless meta_post.entity_id == self.entity_id 80 | 81 | self.update(:meta_post_id => meta_post.id) 82 | @meta_post = meta_post 83 | end 84 | 85 | def reload 86 | super 87 | reload_meta_post 88 | self 89 | end 90 | 91 | def reload_meta_post 92 | @meta_post = Post.first(:id => self.meta_post_id) 93 | end 94 | 95 | def meta_post 96 | @meta_post || reload_meta_post 97 | end 98 | 99 | def preferred_server 100 | meta_post.content['servers'].sort_by { |server| server['preference'] }.first 101 | end 102 | end 103 | 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/tentd/proxied_post.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class ProxiedPost 3 | def initialize(post_json) 4 | @post_json = Utils::Hash.symbolize_keys(post_json, :deep => false) 5 | @post_json[:version] = Utils::Hash.symbolize_keys(@post_json[:version], :deep => false) 6 | end 7 | 8 | def public_id 9 | @post_json[:id] 10 | end 11 | 12 | def entity 13 | @post_json[:entity] 14 | end 15 | 16 | def entity_id 17 | @entity_id ||= begin 18 | _entity = Model::Entity.select(:id).where(:entity => entity).first 19 | _entity.id if _entity 20 | end 21 | end 22 | 23 | def content 24 | @post_json[:content] 25 | end 26 | 27 | def version 28 | @post_json[:version][:id] 29 | end 30 | 31 | def version_parents 32 | @post_json[:version][:parents] 33 | end 34 | 35 | def published_at 36 | @post_json[:published_at] 37 | end 38 | 39 | def received_at 40 | @post_json[:received_at] 41 | end 42 | 43 | def version_published_at 44 | @post_json[:version][:published_at] 45 | end 46 | 47 | def refs 48 | @post_json[:refs] 49 | end 50 | 51 | def mentions 52 | @post_json[:mentions] 53 | end 54 | 55 | def attachments 56 | @post_json[:attachments] 57 | end 58 | 59 | def as_json(options = {}) 60 | @post_json 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/tentd/query.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | 3 | class Query 4 | attr_reader :model, :table_name, :select_columns, :joins, :sort_columns, :query_conditions, :query_bindings 5 | attr_accessor :limit, :reverse_sort, :deleted_at_table_names 6 | def initialize(model) 7 | @model = model 8 | @table_name = model.table_name 9 | @query_conditions = [] 10 | @query_bindings = [] 11 | 12 | if TentD.database[table_name].columns.include?(:deleted_at) 13 | @deleted_at_table_names = [table_name] 14 | else 15 | @deleted_at_table_names = [] 16 | end 17 | 18 | @select_columns = "#{table_name}.*" 19 | @joins = [] 20 | @sort_columns = nil 21 | @reverse_sort = false 22 | end 23 | 24 | def select_columns=(columns) 25 | @select_columns = Array(columns).map(&:to_s).join(',') 26 | end 27 | 28 | def sort_columns=(columns) 29 | @sort_columns = Array(columns).map(&:to_s).join(', ') 30 | end 31 | 32 | def join(sql) 33 | joins << sql 34 | end 35 | 36 | def qualify(column) 37 | "#{table_name}.#{column}" 38 | end 39 | 40 | def build_query_conditions(options = {}) 41 | sep = options[:conditions_sep] || 'AND' 42 | 43 | query_conditions.map do |c| 44 | _build_conditions(c) 45 | end.join(" #{sep} ") 46 | end 47 | 48 | def _build_conditions(conditions) 49 | conditions = conditions.dup 50 | if conditions.kind_of?(Array) && ['OR', 'AND'].include?(conditions.first) 51 | sep = conditions.shift 52 | if conditions.size > 1 53 | conditions.map! {|c| c.kind_of?(Array) ? _build_conditions(c) : c } 54 | "(#{conditions.join(" #{sep} ")})" 55 | else 56 | if conditions.first.kind_of?(Array) 57 | _build_conditions(conditions.first) 58 | else 59 | conditions.first 60 | end 61 | end 62 | else 63 | conditions 64 | end 65 | end 66 | 67 | def to_sql(options = {}) 68 | if options[:count] 69 | q = ["SELECT COUNT(#{options[:select_columns] || select_columns}) AS tally FROM #{table_name}"].concat(joins) 70 | else 71 | q = ["SELECT #{options[:select_columns] || select_columns} FROM #{table_name}"].concat(joins) 72 | end 73 | 74 | _deleted_at_conditions = if Model.soft_delete && deleted_at_table_names.any? 75 | deleted_at_table_names.uniq.map { |t| "#{t}.deleted_at IS NULL" }.join(" AND ") 76 | end 77 | 78 | if query_conditions.any? 79 | if _deleted_at_conditions 80 | q << "WHERE #{build_query_conditions(options)} AND #{_deleted_at_conditions}" 81 | else 82 | q << "WHERE #{build_query_conditions(options)}" 83 | end 84 | elsif _deleted_at_conditions 85 | q << "WHERE #{_deleted_at_conditions}" 86 | end 87 | 88 | unless options[:count] 89 | if sort_columns 90 | if reverse_sort 91 | q << "ORDER BY #{sort_columns.gsub("DESC", "ASC")}" 92 | else 93 | q << "ORDER BY #{sort_columns}" 94 | end 95 | end 96 | 97 | q << "LIMIT #{options[:limit] || limit.to_i}" if limit 98 | end 99 | 100 | q.join(' ') 101 | end 102 | 103 | def count 104 | model.with_sql(to_sql(:select_columns => "#{table_name}.id", :count => true), *query_bindings).to_a.first[:tally] 105 | end 106 | 107 | def any? 108 | model.with_sql(to_sql(:select_columns => "#{table_name}.id", :limit => 1), *query_bindings).to_a.any? 109 | end 110 | 111 | def first 112 | model.with_sql(to_sql(:limit => 1), *query_bindings).to_a.first 113 | end 114 | 115 | def all(options = {}) 116 | models = model.with_sql(to_sql(options), *query_bindings).to_a 117 | if reverse_sort 118 | models.reverse 119 | else 120 | models 121 | end 122 | end 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /lib/tentd/refs.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | 3 | class Refs 4 | MAX_REFS_PER_POST = 5.freeze 5 | 6 | attr_reader :env 7 | def initialize(env) 8 | @env = env 9 | @proxy_clients = {} 10 | end 11 | 12 | def fetch(*posts, max_refs) 13 | fetch_with_proxy(*posts, max_refs) 14 | end 15 | 16 | def fetch_with_proxy(*posts, max_refs) 17 | max_refs = [MAX_REFS_PER_POST, max_refs.to_i].min 18 | return [] if max_refs == 0 19 | 20 | foreign_refs = [] 21 | 22 | q = Query.new(Model::Post) 23 | 24 | q.query_conditions << "#{q.table_name}.user_id = ?" 25 | q.query_bindings << current_user.id 26 | 27 | ref_conditions = [] 28 | posts.each do |post| 29 | # mixture of Model::Post and TentD::ProxiedPost 30 | next unless post.refs.to_a.any? 31 | 32 | post.refs.slice(0, max_refs).each do |ref| 33 | next if ref['entity'] && !can_read?(ref['entity']) 34 | 35 | if ref['entity'] && ref['entity'] != current_user.entity 36 | foreign_refs << ref 37 | end 38 | 39 | ref_conditions << ["AND", 40 | "#{q.table_name}.public_id = ?", 41 | (ref['entity'] || !post.entity_id) ? "#{q.table_name}.entity = ?" : "#{q.table_name}.entity_id = ?" 42 | ] 43 | 44 | q.query_bindings << ref['post'] 45 | q.query_bindings << (ref['entity'] || post.entity_id || post.entity) 46 | end 47 | end 48 | return [] if ref_conditions.empty? 49 | q.query_conditions << ["OR"].concat(ref_conditions) 50 | 51 | unless request_proxy_manager.proxy_condition == :always 52 | reffed_posts = q.all.uniq 53 | else 54 | reffed_posts = [] 55 | end 56 | 57 | unless reffed_posts.size == max_refs 58 | foreign_refs = foreign_refs.inject([]) do |memo, ref| 59 | # skip over refs that are already found 60 | next memo if reffed_posts.any? { |post| 61 | if ref['version'] 62 | post.entity == ref['entity'] && post.public_id == ref['post'] && post.version == ref['version'] 63 | else 64 | post.entity == ref['entity'] && post.public_id == ref['post'] 65 | end 66 | } 67 | 68 | request_proxy_manager.get_post(ref['entity'], ref['post'], ref['version']) do |post| 69 | memo << post 70 | end 71 | 72 | memo 73 | end 74 | else 75 | foreign_refs = [] 76 | end 77 | 78 | reffed_posts.map { |p| p.as_json(:env => env) } + foreign_refs 79 | end 80 | 81 | private 82 | 83 | def can_read?(entity) 84 | auth_candidate = Authorizer.new(env).auth_candidate 85 | auth_candidate && auth_candidate.read_entity?(entity) 86 | end 87 | 88 | def request_proxy_manager 89 | @request_proxy_manager ||= env['request_proxy_manager'] 90 | end 91 | 92 | def current_user 93 | @current_user ||= env['current_user'] 94 | end 95 | 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /lib/tentd/relationship_importer.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class RelationshipImporter 3 | RELATIONSHIP_TYPE = TentType.new(%(https://tent.io/types/relationship/v0#)).freeze 4 | CREDENTIALS_TYPE = TentType.new(%(https://tent.io/types/credentials/v0#)).freeze 5 | 6 | ImportError = Class.new(Model::Post::CreateFailure) 7 | 8 | Results = Struct.new(:post) 9 | 10 | def self.import(current_user, attrs) 11 | # Case 1 (they initiated) 12 | # [author:them] relationship#initial 13 | # - create relationship (entity => attrs[:entity]) 14 | # [author:them] credentials (mentions ^) 15 | # - update relationship with [remote] credentials 16 | # [author:us] relationship# (mentions relationship#initial ^) 17 | # - update relationship with post_id 18 | # [author:us] credentials (mentions ^) 19 | # - update relationship with credentials_post_id 20 | # [author:them] relationship# (mentions relationship# ^) 21 | 22 | # Case 2 (we initiated) 23 | # [author:us] relationship#initial 24 | # - create relationship with post_id (entity => attrs[:mentions][0]['entity']) 25 | # [author:us] credentials (mentions ^) 26 | # - update relationship with credentials_post_id 27 | # [author:them] relationship# (mentions relationship#initial ^) 28 | # [author:them] credentials (mentions ^) 29 | # - update relationship with [remote] credentials 30 | # [author:us] relationship# (mentions relationship# ^) 31 | # - update post_id 32 | 33 | new(current_user, attrs).import 34 | end 35 | 36 | attr_reader :current_user, :attrs, :type, :stage, :target_entity, :target_entity_id, :relationship, :post 37 | def initialize(current_user, attrs) 38 | @current_user, @attrs = current_user, attrs 39 | @type = TentType.new(attrs[:type]) 40 | end 41 | 42 | def import 43 | determine_stage 44 | determine_target_entity 45 | determine_target_entity_id 46 | create_post 47 | update_or_create_relationship 48 | 49 | Results.new(post) 50 | end 51 | 52 | private 53 | 54 | def determine_stage 55 | if type.base == RELATIONSHIP_TYPE.base 56 | if type.fragment == 'initial' 57 | if current_user.entity_id == attrs[:entity_id] 58 | @stage = :local_initial 59 | else 60 | @stage = :remote_initial 61 | end 62 | else 63 | if current_user.entity_id == attrs[:entity_id] 64 | @stage = :local_final 65 | else 66 | @stage = :remote_final 67 | end 68 | end 69 | elsif type.base == CREDENTIALS_TYPE.base 70 | if current_user.entity_id == attrs[:entity_id] 71 | @stage = :local_credentials 72 | else 73 | @stage = :remote_credentials 74 | end 75 | end 76 | end 77 | 78 | def determine_target_entity 79 | @target_entity = case stage 80 | when :local_initial 81 | mention = attrs[:mentions].first 82 | 83 | unless mention 84 | raise ImportError.new("Invalid #{attrs[:type].inspect}: Must mention an entity") 85 | end 86 | 87 | mention['entity'] 88 | when :local_credentials 89 | mention = attrs[:mentions].find { |m| 90 | TentType.new(m['type']).base == RELATIONSHIP_TYPE.base 91 | } 92 | post = Model::Post.where( 93 | :user_id => current_user.id, 94 | :entity_id => current_user.entity_id, 95 | :public_id => mention['post'] 96 | ).first 97 | 98 | unless post 99 | raise ImportError.new("Mentioned relationship post(#{mention['post'].inspect}) not found") 100 | end 101 | 102 | mention = post.mentions.find { |m| 103 | TentType.new(m['type']).base == RELATIONSHIP_TYPE.base 104 | } 105 | 106 | unless mention 107 | raise ImportError.new("Mentioned post(#{mention['post'].inspect}) must mention a relationship post") 108 | end 109 | 110 | mention['entity'] 111 | when :local_final 112 | mention = attrs[:mentions].find { |m| 113 | TentType.new(m['type']).base == RELATIONSHIP_TYPE.base 114 | } 115 | 116 | unless mention 117 | raise ImportError.new("Must mention a relationship post") 118 | end 119 | 120 | mention['entity'] 121 | when :remote_initial 122 | attrs[:entity] 123 | when :remote_credentials 124 | attrs[:entity] 125 | when :remote_final 126 | attrs[:entity] 127 | end 128 | end 129 | 130 | def determine_target_entity_id 131 | @target_entity_id = if target_entity == attrs['entity'] 132 | attrs['entity_id'] 133 | else 134 | Model::Entity.first_or_create(target_entity).id 135 | end 136 | end 137 | 138 | def create_post 139 | @post = Model::Post.create(attrs) 140 | end 141 | 142 | def relationship_attrs 143 | _attrs = { 144 | :entity_id => target_entity_id, 145 | :entity => target_entity 146 | } 147 | 148 | case stage 149 | when :local_initial, :local_final 150 | _attrs[:post_id] = post.id 151 | _attrs[:type_id] = post.type_id 152 | when :local_credentials 153 | _attrs[:credentials_post_id] = post.id 154 | when :remote_credentials 155 | _attrs[:remote_credentials_id] = post.public_id 156 | _attrs[:remote_credentials] = post.content.merge('id' => post.public_id) 157 | end 158 | 159 | _attrs 160 | end 161 | 162 | def update_or_create_relationship 163 | @relationship = find_and_update_relationship || create_relationship 164 | end 165 | 166 | def find_and_update_relationship 167 | if relationship = Model::Relationship.where( 168 | :user_id => current_user.id, 169 | :entity_id => target_entity_id 170 | ).first 171 | 172 | relationship.update(relationship_attrs) 173 | 174 | relationship 175 | end 176 | end 177 | 178 | def create_relationship 179 | Model::Relationship.create(relationship_attrs.merge( 180 | :user_id => current_user.id, 181 | :entity_id => target_entity_id 182 | )) 183 | rescue Sequel::UniqueConstraintViolation 184 | find_and_update_relationship 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /lib/tentd/request_proxy_manager.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | class RequestProxyManager 3 | def self.proxy_client(current_user, entity, options = {}) 4 | if relationship = Model::Relationship.where( 5 | :user_id => current_user.id, 6 | :entity => entity, 7 | ).where(Sequel.~(:remote_credentials_id => nil)).first 8 | relationship.client(options) 9 | else 10 | TentClient.new(entity, options) 11 | end 12 | end 13 | 14 | attr_reader :env 15 | def initialize(env) 16 | @env = env 17 | @proxy_clients = {} 18 | end 19 | 20 | def get_post(entity, id, version = nil, &block) 21 | return unless can_proxy?(entity) 22 | 23 | client = proxy_client(entity) 24 | 25 | params = {} 26 | params[:version] = version if version 27 | 28 | res = client.post.get(entity, id, params) 29 | 30 | if res.status == 200 && (Hash === res.body) && (Hash === res.body['post']) 31 | yield Utils::Hash.symbolize_keys(res.body['post']) 32 | end 33 | 34 | res 35 | rescue Faraday::Error::TimeoutError 36 | rescue Faraday::Error::ConnectionFailed 37 | end 38 | 39 | def can_proxy?(entity) 40 | return false if entity == current_user.entity 41 | proxy_condition != :never && can_read?(entity) 42 | end 43 | 44 | def can_read?(entity) 45 | auth_candidate = Authorizer.new(env).auth_candidate 46 | auth_candidate && auth_candidate.read_entity?(entity) 47 | end 48 | 49 | def proxy_client(entity, options = {}) 50 | self.class.proxy_client(current_user, entity, options) 51 | end 52 | 53 | def request(entity, options = {}, &block) 54 | res = yield(proxy_client(entity, options.merge(:skip_response_serialization => true))) 55 | 56 | body = res.body.respond_to?(:each) ? res.body : [res.body] 57 | 58 | headers = API::PROXY_HEADERS.inject({}) do |memo, key| 59 | memo[key] = res.headers[key] if res.headers[key] 60 | memo 61 | end 62 | 63 | [res.status, headers, body] 64 | rescue Faraday::Error::TimeoutError 65 | res ||= Faraday::Response.new({}) 66 | halt!(504, res) 67 | rescue Faraday::Error::ConnectionFailed 68 | res ||= Faraday::Response.new({}) 69 | halt!(502, res) 70 | end 71 | 72 | def halt!(status, res) 73 | raise API::Middleware::Halt.new(status, "Failed to proxy request: #{res.env[:method].to_s.upcase} #{res.env[:url].to_s}") 74 | end 75 | 76 | def proxy_condition 77 | return :never unless authorizer.proxy_authorized? 78 | 79 | case env['HTTP_CACHE_CONTROL'].to_s 80 | when /\bproxy-if-miss\b/ 81 | :on_miss 82 | when /\bno-proxy\b/ 83 | :never 84 | when /\bproxy\b/ 85 | :always 86 | else 87 | env['request.feed'] ? :never : :on_miss 88 | end 89 | end 90 | 91 | def authorizer 92 | @authorizer ||= Authorizer.new(env) 93 | end 94 | 95 | def current_user 96 | @current_user ||= env['current_user'] 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/tentd/schema_validator.rb: -------------------------------------------------------------------------------- 1 | require 'tent-schemas' 2 | require 'api-validator' 3 | require 'tentd/schema_validator/format_validators' 4 | 5 | module TentD 6 | class SchemaValidator < ApiValidator::JsonSchema 7 | 8 | @schemas = TentSchemas.schemas.inject(Hash.new) do |memo, (name, schema)| 9 | memo[name] = TentSchemas.inject_refs!(schema) 10 | memo 11 | end 12 | 13 | def self.validation(type_uri, data) 14 | type = TentClient::TentType.new(type_uri) 15 | schema_name = "post_#{type.base.to_s.split('/').last}" 16 | 17 | remove_null_members(data) 18 | 19 | # Validate post schema 20 | schema = @schemas['post'].dup 21 | schema['properties'].each_pair { |name, property| property['required'] = false } 22 | v = new(schema) 23 | return v unless v.valid?(data) 24 | 25 | # Don't validate content of unknown post types 26 | return v unless schema = @schemas[schema_name] 27 | 28 | # Validate content of known post types 29 | new(schema, "/content") 30 | end 31 | 32 | def self.diff(type_uri, data) 33 | v = validation(type_uri, data) 34 | v.diff(data, v.failed_assertions(data)) 35 | end 36 | 37 | def self.validate(type_uri, data) 38 | diff(type_uri, data).empty? 39 | end 40 | 41 | def valid?(data) 42 | diff(data, failed_assertions(data)).empty? 43 | end 44 | 45 | private 46 | 47 | def self.remove_null_members(hash) 48 | hash.each_pair do |key, val| 49 | case val 50 | when Hash 51 | remove_null_members(val) 52 | when Array 53 | val.each do |item| 54 | next unless Hash === item 55 | remove_null_members(item) 56 | end 57 | when NilClass 58 | hash.delete(key) 59 | end 60 | end 61 | end 62 | 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/tentd/schema_validator/format_validators.rb: -------------------------------------------------------------------------------- 1 | ApiValidator.format_validators['https://tent.io/formats/authorize-type-uri'] = lambda do |value| 2 | return true if value == 'all' 3 | begin 4 | uri = URI(value) 5 | uri.scheme && uri.host 6 | rescue URI::InvalidURIError, ArgumentError 7 | false 8 | end 9 | end 10 | 11 | ApiValidator.format_validators['https://tent.io/formats/page-uri'] = lambda do |value| 12 | # see pchar format in https://tools.ietf.org/html/rfc3986#appendix-A 13 | value && value =~ /\A\?[-~._,:@%!&"()*+,;=a-z0-9]{0,}\Z/i 14 | end 15 | 16 | ApiValidator.format_validators['uri-template'] = lambda do |value| 17 | return false unless String === value 18 | begin 19 | uri = URI(value.gsub(/[{}]/, '')) 20 | uri.scheme && uri.host 21 | rescue URI::InvalidURIError, ArgumentError 22 | false 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/tentd/sequel/plugins/paranoia.rb: -------------------------------------------------------------------------------- 1 | # Source: https://gist.github.com/1407841 2 | module Sequel 3 | module Plugins 4 | # The paranoia plugin creates hooks that automatically set deleted 5 | # timestamp fields. The field name used is configurable, and you 6 | # can also set whether to overwrite existing deleted timestamps (false 7 | # by default). Adapted from Timestamps plugin. 8 | # 9 | # Usage: 10 | # 11 | # # Soft deletion for all model instances using +deleted_at+ 12 | # # (called before loading subclasses) 13 | # Sequel::Model.plugin :paranoia 14 | # 15 | # # Paranoid Album instances, with custom column names 16 | # Album.plugin :paranoia, :deleted_at=>:deleted_time 17 | # 18 | # # Paranoid Artist instances, forcing an overwrite of the deleted 19 | # # timestamp 20 | # Album.plugin :paranoia, :force=>true 21 | module Paranoia 22 | # Configure the plugin by setting the avialable options. Note that 23 | # if this method is run more than once, previous settings are ignored, 24 | # and it will just use the settings given or the default settings. Options: 25 | # * :deleted_at - The field to hold the deleted timestamp (default: :deleted_at) 26 | # * :force - Whether to overwrite an existing deleted timestamp (default: false) 27 | def self.configure(model, opts={}) 28 | model.instance_eval do 29 | @deleted_timestamp_field = opts[:deleted_at]||:deleted_at 30 | @deleted_timestamp_overwrite = opts[:force]||false 31 | end 32 | model.class_eval do 33 | set_dataset filter(@deleted_timestamp_field => nil) 34 | end 35 | end 36 | 37 | module ClassMethods 38 | # The field to store the deleted timestamp 39 | attr_reader :deleted_timestamp_field 40 | 41 | # Whether to overwrite the deleted timestamp if it already exists 42 | def deleted_timestamp_overwrite? 43 | @deleted_timestamp_overwrite 44 | end 45 | 46 | # Copy the class instance variables used from the superclass to the subclass 47 | def inherited(subclass) 48 | super 49 | [:@deleted_timestamp_field, :@deleted_timestamp_overwrite].each do |iv| 50 | subclass.instance_variable_set(iv, instance_variable_get(iv)) 51 | end 52 | end 53 | 54 | def with_deleted 55 | dataset.unfiltered 56 | end 57 | end 58 | 59 | module InstanceMethods 60 | # Rather than delete the object, update its deleted timestamp field. 61 | def delete 62 | set_deleted_timestamp 63 | end 64 | 65 | private 66 | 67 | # If the object has accessor methods for the deleted timestamp field, and 68 | # the deleted timestamp value is nil or overwriting it is allowed, set the 69 | # deleted timestamp field to the time given or the current time. 70 | def set_deleted_timestamp(time=nil) 71 | field = model.deleted_timestamp_field 72 | meth = :"#{field}=" 73 | if respond_to?(field) && respond_to?(meth) && (model.deleted_timestamp_overwrite? || send(field).nil?) 74 | self.send(meth, time||=Sequel.datetime_class.now) 75 | self.save 76 | end 77 | end 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/tentd/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require 'tentd' 2 | require 'tentd/query' 3 | require 'tentd/worker' 4 | require 'tentd/authorizer' 5 | 6 | TentD::Worker.configure_server 7 | -------------------------------------------------------------------------------- /lib/tentd/tasks/db.rb: -------------------------------------------------------------------------------- 1 | PG_DB_URL_REGEXP = %r{\A(?:postgres://(?:([^:]+):)?(?:([^@]+)@)?([^/]+)/)?(.+)\Z}.freeze 2 | 3 | namespace :db do 4 | task :migrate do 5 | path = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'db', 'migrations')) 6 | system("bundle exec sequel -m #{path} #{ENV['DATABASE_URL']}") 7 | end 8 | 9 | task :create do 10 | exit 1 unless m = ENV['DATABASE_URL'].to_s.match(PG_DB_URL_REGEXP) 11 | opts = { 12 | :username => m[1], 13 | :host => m[3], 14 | }.inject([]) { |m, (k,v)| next m unless v; m << %(--#{k}="#{v}"); m }.join(" ") 15 | dbname = m[4] 16 | 17 | system("createdb #{opts} #{dbname}") 18 | end 19 | 20 | task :drop do 21 | exit 1 unless m = ENV['DATABASE_URL'].to_s.match(PG_DB_URL_REGEXP) 22 | opts = { 23 | :username => m[1], 24 | :host => m[3], 25 | }.inject([]) { |m, (k,v)| next m unless v; m << %(--#{k}="#{v}"); m }.join(" ") 26 | dbname = m[4] 27 | 28 | system("dropdb #{opts} #{dbname}") 29 | end 30 | 31 | task :setup => [:create, :migrate] do 32 | end 33 | 34 | task :reset => [:drop, :setup] do 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/tentd/utils.rb: -------------------------------------------------------------------------------- 1 | require 'hawk' 2 | 3 | module TentD 4 | module Utils 5 | 6 | MAC_ALGORITHM = "sha256".freeze 7 | 8 | def self.random_id 9 | SecureRandom.urlsafe_base64(16) 10 | end 11 | 12 | def self.hawk_key 13 | SecureRandom.hex(32) 14 | end 15 | 16 | def self.hawk_algorithm 17 | MAC_ALGORITHM 18 | end 19 | 20 | def self.hex_digest(io) 21 | io = StringIO.new(io) if String === io 22 | 23 | digest = Digest::SHA512.new 24 | while buffer = io.read(1024) 25 | digest << buffer 26 | end 27 | io.rewind 28 | "sha512t256-" + digest.hexdigest[0...64] 29 | end 30 | 31 | def self.timestamp 32 | (Time.now.to_f * 1000).to_i 33 | end 34 | 35 | def self.expand_uri_template(template, params = {}) 36 | template.to_s.gsub(/{([^}]+)}/) { URI.encode_www_form_component(params[$1] || params[$1.to_sym]) } 37 | end 38 | 39 | def self.sign_url(credentials, url, options = {}) 40 | credentials = Hash.symbolize_keys(credentials) 41 | 42 | options[:ttl] ||= 86400 # 24 hours 43 | options[:method] ||= 'GET' 44 | 45 | uri = URI(url) 46 | options.merge!( 47 | :credentials => { 48 | :id => credentials[:id], 49 | :key => credentials[:hawk_key], 50 | :algorithm => credentials[:hawk_algorithm] 51 | }, 52 | :host => uri.host, 53 | :port => uri.port || (uri.scheme == 'https' ? 443 : 80), 54 | :request_uri => uri.path + (uri.query ? "?#{uri.query}" : '') 55 | ) 56 | 57 | bewit = Hawk::Crypto.bewit(options) 58 | uri.query ? uri.query += "&bewit=#{bewit}" : uri.query = "bewit=#{bewit}" 59 | uri.to_s 60 | end 61 | 62 | module Hash 63 | extend self 64 | 65 | def deep_dup(item) 66 | case item 67 | when ::Hash 68 | item.inject({}) do |memo, (k,v)| 69 | memo[k] = deep_dup(v) 70 | memo 71 | end 72 | when Array 73 | item.map { |i| deep_dup(i) } 74 | when Symbol, TrueClass, FalseClass, NilClass, Numeric 75 | item 76 | else 77 | item.respond_to?(:dup) ? item.dup : item 78 | end 79 | end 80 | 81 | def deep_merge!(hash, *others) 82 | others.each do |other| 83 | other.each_pair do |key, val| 84 | if hash.has_key?(key) 85 | next if hash[key] == val 86 | case val 87 | when ::Hash 88 | Utils::Hash.deep_merge!(hash[key], val) 89 | when Array 90 | hash[key].concat(val) 91 | when FalseClass 92 | # false always wins 93 | hash[key] = val 94 | end 95 | else 96 | hash[key] = val 97 | end 98 | end 99 | end 100 | end 101 | 102 | def slice(hash, *keys) 103 | keys.each_with_object(hash.class.new) { |k, new_hash| 104 | new_hash[k] = hash[k] if hash.has_key?(k) 105 | } 106 | end 107 | 108 | def slice!(hash, *keys) 109 | hash.replace(slice(hash, *keys)) 110 | end 111 | 112 | def stringify_keys(hash, options = {}) 113 | transform_keys(hash, :to_s, options).first 114 | end 115 | 116 | def stringify_keys!(hash, options = {}) 117 | hash.replace(stringify_keys(hash, options)) 118 | end 119 | 120 | def symbolize_keys(hash, options = {}) 121 | transform_keys(hash, :to_sym, options).first 122 | end 123 | 124 | def symbolize_keys!(hash, options = {}) 125 | hash.replace(symbolize_keys(hash, options)) 126 | end 127 | 128 | def transform_keys(*items, method, options) 129 | items.map do |item| 130 | case item 131 | when ::Hash 132 | item.inject(::Hash.new) do |new_hash, (k,v)| 133 | new_hash[k.send(method)] = (options[:deep] != false) ? transform_keys(v, method, options).first : v 134 | new_hash 135 | end 136 | when ::Array 137 | item.map { |i| transform_keys(i, method, options).first } 138 | else 139 | item 140 | end 141 | end 142 | end 143 | end 144 | 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/tentd/version.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | VERSION = '0.2.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/tentd/worker.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Worker 3 | 4 | class RetryCount 5 | def call(worker, msg, queue) 6 | if worker.respond_to?(:retry_count=) 7 | worker.retry_count = msg['retry_count'] || 0 8 | end 9 | 10 | yield 11 | end 12 | end 13 | 14 | require 'sidekiq' 15 | 16 | def self.configure_client(redis_opts = {}, &block) 17 | Sidekiq.configure_client do |config| 18 | config.redis = { :namespace => ENV['REDIS_NAMESPACE'], :size => 1, :url => ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] }.merge(redis_opts) 19 | yield(config) if block_given? 20 | end 21 | end 22 | 23 | def self.configure_server(redis_opts = {}, &block) 24 | require 'sequel' 25 | Sequel.single_threaded = false 26 | 27 | TentD.setup_database! 28 | 29 | Sidekiq.configure_server do |config| 30 | config.redis = { :namespace => ENV['REDIS_NAMESPACE'], :url => ENV['REDIS_URL'] || ENV['REDISCLOUD_URL'] }.merge(redis_opts) 31 | 32 | config.server_middleware do |chain| 33 | chain.add RetryCount 34 | end 35 | 36 | yield(config) if block_given? 37 | end 38 | end 39 | 40 | def self.run_server 41 | sidekiq_pid = fork do 42 | begin 43 | require 'sidekiq/cli' 44 | require 'tentd' 45 | 46 | STDOUT.reopen(ENV['SIDEKIQ_LOG'] || STDOUT) 47 | STDERR.reopen(ENV['SIDEKIQ_LOG'] || STDERR) 48 | 49 | Sidekiq.options[:require] = File.join(File.expand_path(File.dirname(__FILE__)), 'sidekiq.rb') # tentd/sidekiq 50 | Sidekiq.options[:logfile] = ENV['SIDEKIQ_LOG'] 51 | 52 | args = [] 53 | args.push('--verbose') if TentD.settings[:debug] 54 | 55 | cli = Sidekiq::CLI.instance 56 | cli.parse(args) 57 | cli.run 58 | rescue => e 59 | raise e if $DEBUG 60 | STDERR.puts e.message 61 | STDERR.puts e.backtrace.join("\n") 62 | exit 1 63 | end 64 | end 65 | sidekiq_pid 66 | end 67 | 68 | require 'tentd/worker/relationship_initiation' 69 | require 'tentd/worker/notification_dispatch' 70 | require 'tentd/worker/notification_app_deliverer' 71 | require 'tentd/worker/notification_deliverer' 72 | 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/tentd/worker/notification_app_deliverer.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Worker 3 | 4 | class NotificationAppDeliverer 5 | include Sidekiq::Worker 6 | 7 | sidekiq_options :retry => 10 8 | 9 | DeliveryFailure = Class.new(StandardError) 10 | 11 | MAX_RELATIONSHIP_RETRY = 10.freeze 12 | 13 | def perform(post_id, app_id) 14 | unless post = Model::Post.where(:id => post_id).first 15 | logger.info "Post(#{post_id}) deleted" 16 | return 17 | end 18 | 19 | unless app = Model::App.where(:id => app_id).first 20 | logger.info "App(#{app_id}) deleted" 21 | return 22 | end 23 | 24 | unless app_credentials = Model::Post.where(:id => app.credentials_post_id).first 25 | logger.info "App(#{app_id}) credentials Post(#{app.credentials_post_id.inspect}) missing" 26 | return 27 | end 28 | 29 | logger.info "Delivering Post(#{post_id}) to App(#{app_id})" 30 | 31 | client = TentClient.new(nil, :credentials => Model::Credentials.slice_credentials(app_credentials)) 32 | res = client.http.put(app.notification_url, {}, post.as_json( 33 | :env => { 34 | 'current_auth' => app_credentials, 35 | 'current_auth.resource' => app 36 | } 37 | )) do |request| 38 | request.headers['Content-Type'] = %(application/vnd.tent.post.v0+json; type="%s"; rel="https://tent.io/rels/notification") % post.type 39 | end 40 | 41 | unless res.status == 200 42 | logger.error "Failed to deliver Post(#{post_id}) to App(#{app_id}): #{res.status} #{res.headers.inspect} #{res.body}" 43 | end 44 | 45 | rescue URI::InvalidURIError => e 46 | logger.error "Failed to deliver Post(#{post_id}) to App(#{app_id}): #{e.inspect} #{app.notification_url.inspect}" 47 | raise 48 | end 49 | 50 | def retries_exhausted(post_id, entity) 51 | # TODO: update delivery failure post 52 | end 53 | end 54 | 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/tentd/worker/notification_deliverer.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Worker 3 | 4 | class NotificationDeliverer 5 | include Sidekiq::Worker 6 | 7 | sidekiq_options :retry => 10 8 | 9 | DeliveryFailure = Class.new(StandardError) 10 | EntityUnreachable = Class.new(DeliveryFailure) 11 | RelationshipNotFound = Class.new(DeliveryFailure) 12 | 13 | MAX_RELATIONSHIP_RETRY = 10.freeze 14 | 15 | attr_accessor :retry_count 16 | 17 | def perform(post_id, entity, entity_id=nil, relationship_retry = nil) 18 | logger.info "Attempting to deliver Post(#{post_id} to Entity(#{entity})" 19 | 20 | unless post = Model::Post.where(:id => post_id).first 21 | logger.info "Post(#{post_id}) deleted" 22 | return 23 | end 24 | 25 | unless entity_id 26 | entity_id = Model::Entity.first_or_create(entity).id 27 | end 28 | 29 | q = Model::Relationship.where(:user_id => post.user_id, :entity_id => entity_id) 30 | relationship = q.where(Sequel.~(:remote_credentials_id => nil)).first || q.first 31 | 32 | if relationship && !relationship.active 33 | relationship_retry ||= { 'retries' => 0 } 34 | 35 | if relationship_retry['retries'] >= MAX_RELATIONSHIP_RETRY 36 | # no viable relationship after 396 seconds 37 | # raise error and let sidekiq take over with a more aggressive backoff 38 | raise RelationshipNotFound.new("No viable Relationship(#{post.user_id}, #{entity_id})") 39 | else 40 | # slowly backoff (1, 2, 5, 10, 17, 26, 37, 50, 65, 82, and 101 seconds) 41 | delay = 1 + (relationship_retry['retries'] ** 2) 42 | 43 | logger.warn "Failed to deliver Post(#{post_id}) to Entity(#{entity}), No viable relationship exists. Will retry in #{delay}s." 44 | 45 | relationship_retry['retries'] += 1 46 | NotificationDeliverer.perform_in(delay, post_id, entity, entity_id, relationship_retry) 47 | return 48 | end 49 | end 50 | 51 | unless relationship 52 | logger.info "Creating relationship to deliver Post(#{post_id}) to Entity(#{entity})." 53 | 54 | RelationshipInitiation.perform_async(post.user_id, entity_id, post_id) 55 | return 56 | end 57 | 58 | client = relationship.client 59 | 60 | current_user = Model::User.where(:id => post.user_id).first 61 | 62 | res = client.post.update(post.entity, post.public_id, post.as_json(:delivery => true), {}, :notification => true) 63 | 64 | if res.status == 200 65 | logger.info "Delivered Post(#{post_id}) to Entity(#{entity})" 66 | else 67 | if res.status > 500 68 | error_class = EntityUnreachable 69 | else 70 | error_class = DeliveryFailure 71 | end 72 | 73 | raise error_class.new("Failed deliver post(id: #{post.id}) via #{res.env[:method].to_s.upcase} #{res.env[:url].to_s}\nREQUEST_BODY: #{post.as_json.inspect}\nRESPONSE_BODY: #{res.body.inspect}\nSTATUS: #{res.status.inspect}") 74 | end 75 | rescue EntityUnreachable 76 | if retry_count == 0 77 | delivery_failure(entity, post, "temporary", "unreachable") 78 | end 79 | 80 | raise 81 | rescue DeliveryFailure 82 | if retry_count == 0 83 | delivery_failure(entity, post, "temporary", "delivery_failed") 84 | end 85 | 86 | raise 87 | end 88 | 89 | def retries_exhausted(post_id, entity) 90 | return unless post = Model::Post.where(:id => post_id).first 91 | 92 | existing_delivery_failure = Model::DeliveryFailure.where( 93 | :user_id => post.user_id, 94 | :failed_post_id => post.id, 95 | :entity => entity 96 | ).first 97 | 98 | reason = existing_delivery_failure ? existing_delivery_failure.reasion : 'delivery_failed' 99 | delivery_failure(entity, post, "permanent", reason) 100 | end 101 | 102 | private 103 | 104 | def delivery_failure(target_entity, post, status, reason) 105 | unless post.mentions.to_a.any? { |m| m['entity'] == target_entity } 106 | logger.info "Delivery failed: Post(#{post.id}) Entity(#{target_entity})" 107 | return 108 | end 109 | 110 | logger.info "Creating #{status.inspect} delivery failure for Post(#{post.id}) to Entity(#{target_entity}): #{reason}" 111 | 112 | Model::DeliveryFailure.find_or_create(target_entity, post, status, reason) 113 | end 114 | end 115 | 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/tentd/worker/notification_dispatch.rb: -------------------------------------------------------------------------------- 1 | module TentD 2 | module Worker 3 | 4 | class NotificationDispatch 5 | include Sidekiq::Worker 6 | 7 | def perform(post_id) 8 | # - when public 9 | # - lookup matching subscriptions / relationships 10 | # - lookup or create relationships with mentioned entities 11 | # - queue delivery to these relationships 12 | # - when private 13 | # - lookup subscriptions / relationships for entities in permissions.entities or belong to a group in permissions.groups 14 | # - lookup or create relationships for mentioned entities which are also in permissions.entities or belong to a group in permissions.groups 15 | 16 | return unless post = Model::Post.where(:id => post_id).first 17 | return unless post.deliverable? 18 | 19 | mentioned_entities = post.mentions.to_a.map { |m| m['entity'] }.compact 20 | 21 | unless post.public 22 | mentioned_entities = post.permissions_entities.to_a & mentioned_entities 23 | 24 | # TODO: permissions.groups 25 | end 26 | 27 | # Lookup all subscriptions linked to a relationship 28 | # where the type matches post.type 29 | # and someone other than us created the subscription 30 | subscriptions = Model::Subscription.where( 31 | :user_id => post.user_id 32 | ).where( 33 | Sequel.|({ :type_id => [post.type_id, post.type_base_id] }, { :type => 'all' }) 34 | ).where( 35 | Sequel.~(:subscriber_entity_id => post.entity_id) 36 | ).qualify.join(:relationships, :relationships__entity_id => :subscriptions__subscriber_entity_id) 37 | 38 | logger.info "#{subscriptions.sql}" 39 | 40 | subscriptions = subscriptions.all.to_a 41 | 42 | logger.info "Found #{subscriptions.size} subscriptions for Post(#{post_id})" 43 | 44 | # get rid of duplicates 45 | subscriptions.uniq! { |s| s.subscriber_entity_id } 46 | 47 | # queue delivery for each subscription 48 | subscriptions.each do |subscription| 49 | NotificationDeliverer.perform_async(post_id, subscription.subscriber_entity, subscription.subscriber_entity_id) 50 | end 51 | 52 | # exclude entities matching a subscription 53 | mentioned_entities -= subscriptions.map(&:entity) 54 | 55 | # don't attempt to deliver notification to ourself 56 | mentioned_entities -= [post.entity] 57 | 58 | logger.info "Found #{mentioned_entities.size} mentioned entities for Post(#{post_id})" 59 | 60 | # queue delivery for each mentioned entity 61 | mentioned_entities.each do |entity| 62 | NotificationDeliverer.perform_async(post_id, entity) 63 | end 64 | 65 | # queue delivery for each subscribed app 66 | subscribed_apps = Model::App.subscribers(post, :select => :id) 67 | 68 | logger.info "Found #{subscribed_apps.size} app subscriptions for Post(#{post_id})" 69 | 70 | subscribed_apps.each do |app| 71 | NotificationAppDeliverer.perform_async(post_id, app.id) 72 | end 73 | end 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /sidekiq.rb: -------------------------------------------------------------------------------- 1 | require 'tentd/sidekiq' 2 | -------------------------------------------------------------------------------- /tentd.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'tentd/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "tentd" 8 | gem.version = TentD::VERSION 9 | gem.authors = ["Jonathan Rudenberg", "Jesse Stuart"] 10 | gem.email = ["jonathan@titanous.com", "jessestuart@gmail.com"] 11 | gem.description = %q{Tent Protocol server reference implementation} 12 | gem.summary = %q{Tent Protocol server reference implementation} 13 | gem.homepage = "http://tent.io" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_runtime_dependency 'rack-putty' 21 | gem.add_runtime_dependency 'tent-client' 22 | gem.add_runtime_dependency 'yajl-ruby' 23 | gem.add_runtime_dependency 'sequel_pg' 24 | gem.add_runtime_dependency 'pg' 25 | gem.add_runtime_dependency 'sequel', '3.46.0' 26 | gem.add_runtime_dependency 'sequel-json' 27 | gem.add_runtime_dependency 'sequel-pg_array' 28 | gem.add_runtime_dependency 'tent-schemas' 29 | gem.add_runtime_dependency 'api-validator' 30 | gem.add_runtime_dependency 'tent-canonical-json' 31 | gem.add_runtime_dependency 'hawk-auth' 32 | gem.add_runtime_dependency 'sidekiq' 33 | gem.add_runtime_dependency 'unicorn' 34 | gem.add_runtime_dependency 'fog' 35 | 36 | gem.add_development_dependency 'bundler' 37 | gem.add_development_dependency 'rake' 38 | gem.add_development_dependency 'puma', '2.0.1' 39 | gem.add_development_dependency 'tent-validator', '~> 0.2.0' 40 | end 41 | --------------------------------------------------------------------------------