If you are the application owner check the logs for more information.
56 | 57 | 58 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Maybe you tried to change something you didn't have access to.
55 |If you are the application owner check the logs for more information.
57 | 58 | 59 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |You may have mistyped the address or the page may have moved.
55 |If you are the application owner check the logs for more information.
57 | 58 | 59 | -------------------------------------------------------------------------------- /app/assets/stylesheets/color.sass: -------------------------------------------------------------------------------- 1 | // 2013 2 | $salmon: #fa8072 3 | $blue: hsl(200, 100%, 50%) 4 | $pinku: #ff0080 5 | $parchment: #ffe 6 | $green: hsl(120, 100%, 50%) 7 | $darkgreen: darken($green, 50%) 8 | 9 | // 2014 10 | $dark_purple: #1E0533 11 | $purple: #4A2966 12 | $light_purple: #DAB7FF 13 | $distant_brown: rgb(67, 39, 32) 14 | $brown: #332105 15 | $dark_red: darken(#f00, 25%) 16 | 17 | // 2015 18 | $amber: #F5B34A 19 | $bright_amber: lighten($amber, 30%) 20 | $light_amber: lighten($amber, 15%) 21 | $dark_amber: darken($amber, 55%) 22 | $darker_amber: darken($amber, 75%) 23 | $crimson: #EB242E 24 | $dark_crimson: darken($crimson, 35%) 25 | $darker_crimson: darken($crimson, 50%) 26 | 27 | // 2016 28 | $teeth: #e7d0c6 29 | $tan: #d19269 30 | $honky: #f3c6b5 31 | $shirt: #d4e1f4 32 | $lefty: #493733 33 | $bluegrey: #817d89 34 | $light_bluegrey: lighten($bluegrey, 25%) 35 | $tielight: #8b858b 36 | $light_tielight: lighten($tielight, 25%) 37 | 38 | // 2017 39 | $pale_mustard: #f1d474 40 | $mustard: #bd8619 41 | $baby_blue: #5037f0 42 | $grey_grape: #7957ba 43 | $grape: #88198D 44 | $dark_grape: darken($grape, 15%) 45 | $superdark_grape: darken($grape, 25%) 46 | $superpale_blue: lighten($baby_blue, 38%) 47 | $paper_average: #cebea8 48 | 49 | $background_color: $dark_grape 50 | 51 | // useful 52 | $deemphasized: desaturate($mustard, 45%) 53 | $emphasized: saturate($mustard, 15%) 54 | 55 | $error: $crimson 56 | $error_bright: #faa 57 | $error_super: #fcc 58 | 59 | $content_background: $dark_grape 60 | $content_text: white 61 | $content_highlight: $light_amber 62 | 63 | a 64 | &:link, &:visited 65 | color: darken($honky, 35%) 66 | -------------------------------------------------------------------------------- /app/views/admin/replacements/show.html.haml: -------------------------------------------------------------------------------- 1 | %p#notice= notice 2 | 3 | %p 4 | %b Team: 5 | = @replacement.team.name 6 | %p 7 | %b Service: 8 | = @replacement.service.name 9 | %p 10 | %b Round: 11 | = @replacement.round_id 12 | %p 13 | %b Digest: 14 | = @replacement.digest 15 | 16 | %h1 Same as 17 | %table 18 | %thead 19 | %tr 20 | %th id 21 | %th round 22 | %th team 23 | %th activities 24 | %tbody 25 | - @sames.each do |r| 26 | %tr 27 | %td= link_to r.id, admin_replacement_path(r) 28 | %td= r.round_id 29 | %td= r.team.name 30 | %td 31 | = link_to 'show', admin_replacement_path(r) 32 | 33 | %h1 This team's 34 | %table 35 | %thead 36 | %tr 37 | %th id 38 | %th round 39 | %th digest 40 | %th activities 41 | %tbody 42 | - @teams.each do |r| 43 | %tr 44 | %td= link_to r.id, admin_replacement_path(r) 45 | %td= r.round_id 46 | %td=r.digest.truncate(16) 47 | %td 48 | = link_to 'show', admin_replacement_path(r) 49 | = 'this one' if r.id == @replacement.id 50 | = 'after this one' if r.id == @next&.id 51 | 52 | %h1 Redemptions against me 53 | %table 54 | %thead 55 | %tr 56 | %th id 57 | %th round 58 | %th scorer 59 | %th activities 60 | %tbody 61 | - @redemptions.each do |red| 62 | %tr 63 | %td= red.id 64 | %td= red.round_id 65 | %td= red.team.name 66 | %td blah 67 | 68 | = link_to 'Edit', edit_replacement_path(@replacement) 69 | \| 70 | = link_to 'Back', replacements_path 71 | -------------------------------------------------------------------------------- /db/migrate/20140807162135_forbid_nulls.rb: -------------------------------------------------------------------------------- 1 | class ForbidNulls < ActiveRecord::Migration 2 | def change 3 | change_table :availabilities do |t| 4 | t.change :instance_id, :integer, null: false 5 | t.change :round_id, :integer, null: false 6 | end 7 | 8 | change_table :captures do |t| 9 | t.change :redemption_id, :integer, null: false 10 | t.change :flag_id, :integer, null: false 11 | t.change :round_id, :integer, null: false 12 | end 13 | 14 | change_table :flags do |t| 15 | t.change :team_id, :integer, null: false 16 | t.change :service_id, :integer, null: false 17 | end 18 | 19 | change_table :instances do |t| 20 | t.change :team_id, :integer, null: false 21 | t.change :service_id, :integer, null: false 22 | end 23 | 24 | change_table :penalties do |t| 25 | t.change :availability_id, :integer, null: false 26 | t.change :team_id, :integer, null: false 27 | t.change :flag_id, :integer, null: false 28 | end 29 | 30 | change_table :redemptions do |t| 31 | t.change :team_id, :integer, null: false 32 | t.change :token_id, :integer, null: false 33 | t.change :round_id, :integer, null: false 34 | t.change :uuid, :uuid, null: false, unique: true 35 | end 36 | 37 | change_table :tickets do |t| 38 | t.change :team_id, :integer, null: false 39 | end 40 | 41 | change_table :tokens do |t| 42 | t.change :instance_id, :integer, null: false 43 | t.change :round_id, :integer, null: false 44 | t.change :key, :string, unique: true, null: false 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/views/admin/instances/show.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Instance: #{@instance.team.name} #{@instance.service.name} 2 | 3 | %table 4 | %tbody 5 | %tr 6 | %th team 7 | %td= @instance.team.name 8 | %tr 9 | %th service 10 | %td= @instance.service.name 11 | %tr 12 | %th flags 13 | %td= @instance.flags.count 14 | 15 | %h2 Recent Tokens 16 | 17 | %table 18 | %thead 19 | %tr 20 | %th id 21 | %th round 22 | %th when 23 | %th deposit status 24 | %th redemptions 25 | %th activities 26 | %tbody 27 | - @instance.tokens.order(id: :desc).limit(50).each do |t| 28 | %tr 29 | %td= t.id 30 | %td= t.round_id 31 | %td 32 | = time_ago t.created_at 33 | %td 34 | - if t.status == 0 35 | ok 36 | - else 37 | failed 38 | = t.status 39 | %td= t.redemptions.count 40 | %td 41 | = link_to 'show', admin_token_path(t) 42 | 43 | %h2 Recent Availability Checks 44 | 45 | %table 46 | %thead 47 | %tr 48 | %th id 49 | %th round 50 | %th when 51 | %th healthy? 52 | %th penalties 53 | %th activities 54 | %tbody 55 | - @instance.availabilities.order(created_at: :desc).limit(50).each do |a| 56 | %tr 57 | %td= a.id 58 | %td= a.round_id 59 | %td{title: a.created_at} 60 | = time_ago_in_words a.created_at, include_seconds: true 61 | ago 62 | %td= a.healthy? 63 | %td= a.penalties.count 64 | %td 65 | = link_to 'show', admin_availability_path(a) 66 | -------------------------------------------------------------------------------- /config/deploy/staging.rb: -------------------------------------------------------------------------------- 1 | # Simple Role Syntax 2 | # ================== 3 | # Supports bulk-adding hosts to roles, the primary server in each group 4 | # is considered to be the first unless any hosts have the primary 5 | # property set. Don't declare `role :all`, it's a meta role. 6 | 7 | role :app, %w{deploy@example.com} 8 | role :web, %w{deploy@example.com} 9 | role :db, %w{deploy@example.com} 10 | 11 | 12 | # Extended Server Syntax 13 | # ====================== 14 | # This can be used to drop a more detailed server definition into the 15 | # server list. The second argument is a, or duck-types, Hash and is 16 | # used to set extended properties on the server. 17 | 18 | server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value 19 | 20 | 21 | # Custom SSH Options 22 | # ================== 23 | # You may pass any option but keep in mind that net/ssh understands a 24 | # limited set of options, consult[net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start). 25 | # 26 | # Global options 27 | # -------------- 28 | # set :ssh_options, { 29 | # keys: %w(/home/rlisowski/.ssh/id_rsa), 30 | # forward_agent: false, 31 | # auth_methods: %w(password) 32 | # } 33 | # 34 | # And/or per server (overrides global) 35 | # ------------------------------------ 36 | # server 'example.com', 37 | # user: 'user_name', 38 | # roles: %w{web app}, 39 | # ssh_options: { 40 | # user: 'user_name', # overrides user setting above 41 | # keys: %w(/home/user_name/.ssh/id_rsa), 42 | # forward_agent: false, 43 | # auth_methods: %w(publickey password) 44 | # # password: 'please use keys' 45 | # } 46 | -------------------------------------------------------------------------------- /app/models/token_redistributor.rb: -------------------------------------------------------------------------------- 1 | class TokenRedistributor 2 | attr_reader :movement_json, :round 3 | def initialize 4 | @movement_json = { } 5 | end 6 | 7 | def process_movements(round) 8 | @round = round 9 | 10 | Service.where(enabled: true).each do |svc| 11 | process_movements_for_service svc 12 | end 13 | end 14 | 15 | def as_movement_json 16 | return { redistribution: @movement_json } 17 | end 18 | 19 | def ==(other) 20 | return false unless other.is_a? self.class 21 | return false unless other.movement_json == self.movement_json 22 | return false unless other.round == self.round 23 | 24 | true 25 | end 26 | 27 | private 28 | def process_movements_for_service(svc) 29 | instances = svc.instances 30 | recipients = instances.map do |i| 31 | i.redemptions.where(round: @round).all 32 | end.flatten.map(&:team).uniq 33 | 34 | @movement_json[svc.id] = record = { } 35 | 36 | record[:recipients] = recipients.as_json only: %i{ id certname } 37 | 38 | return if recipients.count == 0 39 | 40 | flags = Team.legitbs.flags.where(service: svc).to_a 41 | return if flags.count < recipients.count 42 | 43 | bounty = flags.count / recipients.count 44 | 45 | record[:flags] = flags.as_json only: :id 46 | record[:bounty] = bounty 47 | record[:movements] = { } 48 | 49 | recipients.each do |r| 50 | bounty.times do 51 | candidate = flags.pop 52 | candidate.update_attribute :team_id, r.id 53 | 54 | record[:movements][r.id] ||= [] 55 | record[:movements][r.id] << candidate.id 56 | end 57 | end 58 | 59 | @movement_json[svc.name] = record 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/controllers/redemption_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RedemptionControllerTest < ActionController::TestCase 4 | context 'redeeming tokens' do 5 | setup do 6 | @round = FactoryGirl.create :round 7 | @current_team = FactoryGirl.create :team 8 | @request.headers['HTTP_SSL_CLIENT_S_DN_CN'] = @current_team.uuid 9 | end 10 | 11 | should 'return json on an empty token set' do 12 | @token = FactoryGirl.create :token 13 | 14 | post :create, params: { tokens: [] } 15 | 16 | assert_response :success 17 | assert_equal Hash.new, JSON.parse(response.body) 18 | end 19 | 20 | should 'return json and a valid redemption with a single token' do 21 | @token = FactoryGirl.create :token 22 | @ts = @token.to_token_string 23 | 24 | post :create, params: { tokens: [@ts] } 25 | 26 | assert_response :success 27 | assert_equal({@ts => @token.redemptions.first.uuid}, JSON.parse(response.body)) 28 | end 29 | 30 | should 'return json with error messages' do 31 | @token = FactoryGirl.create :token 32 | (Token::EXPIRATION + 1).times{ FactoryGirl.create :round } 33 | @ts = @token.to_token_string 34 | 35 | @token2 = FactoryGirl.create :token 36 | @ts2 = @token2.to_token_string 37 | @existing_redemption = Redemption.redeem_for @current_team, @ts2 38 | 39 | post :create, params: { tokens: [@ts, @ts2] } 40 | 41 | assert_response :success 42 | assert_equal({ 43 | @ts => 'error: Token too old', 44 | @ts2 => 'error: Already redeemed that token' 45 | }, JSON.parse(response.body)) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /config/deploy/production.rb: -------------------------------------------------------------------------------- 1 | set :deploy_to, '/home/scorebot/production' 2 | 3 | # Simple Role Syntax 4 | # ================== 5 | # Supports bulk-adding hosts to roles, the primary server in each group 6 | # is considered to be the first unless any hosts have the primary 7 | # property set. Don't declare `role :all`, it's a meta role. 8 | 9 | role :app, %w{scorebot@10.3.1.7 scorebot@10.3.1.8} 10 | role :web, %w{scorebot@10.3.1.7 scorebot@10.3.1.8} 11 | role :db, %w{scorebot@10.3.1.8} 12 | 13 | 14 | # Extended Server Syntax 15 | # ====================== 16 | # This can be used to drop a more detailed server definition into the 17 | # server list. The second argument is a, or duck-types, Hash and is 18 | # used to set extended properties on the server. 19 | 20 | #server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value 21 | 22 | # Custom SSH Options 23 | # ================== 24 | # You may pass any option but keep in mind that net/ssh understands a 25 | # limited set of options, consult[net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start). 26 | # 27 | # Global options 28 | # -------------- 29 | set :ssh_options, { 30 | keys: %w(/Volumes/ctf2015/.deploy_rsa), 31 | forward_agent: false, 32 | # auth_methods: %w(password) 33 | } 34 | # 35 | # And/or per server (overrides global) 36 | # ------------------------------------ 37 | # server 'example.com', 38 | # user: 'user_name', 39 | # roles: %w{web app}, 40 | # ssh_options: { 41 | # user: 'user_name', # overrides user setting above 42 | # keys: %w(/home/user_name/.ssh/id_rsa), 43 | # forward_agent: false, 44 | # auth_methods: %w(publickey password) 45 | # # password: 'please use keys' 46 | # } 47 | -------------------------------------------------------------------------------- /app/views/replacements/index.html.haml: -------------------------------------------------------------------------------- 1 | .scene 2 | %h1 Replacements 3 | 4 | %p= link_to 'Upload new replacement', new_replacement_path 5 | 6 | #filters 7 | %h2 8 | Filters 9 | = link_to 'Reset', replacements_path, class: 'btn' 10 | %nav#teams 11 | %ul 12 | %li.label teams 13 | - @teams.each do |t| 14 | %li= link_to(t.certname, 15 | @filters.merge(team_id: t.id), 16 | title: t.name, 17 | data: {team_id: t.id}, 18 | class: ((t.id == @filters[:team_id]) ? 'active' : '')) 19 | %nav#services 20 | %ul 21 | %li.label services 22 | - @services.each do |s| 23 | %li= link_to(s.name, 24 | @filters.merge(service_id: s.id), 25 | class: ((s.id == @filters[:service_id]) ? 'active' : '')) 26 | 27 | %nav#pages 28 | %ul 29 | %li.label pages 30 | %li= link_to('prev page', 31 | @filters.merge(page: @page - 1)) 32 | %li= link_to('next page', 33 | @filters.merge(page: @page + 1)) 34 | 35 | - if @replacements.empty? 36 | %p No replacements found. 37 | - else 38 | %table 39 | %thead 40 | %tr 41 | %th id 42 | %th team 43 | %th service 44 | %th hash 45 | %th size 46 | %th actions 47 | %tbody 48 | - @replacements.each do |r| 49 | %tr 50 | %td= r.id 51 | %td{title: r.team.name}= r.team.certname 52 | %td= r.service.name 53 | %td= r.digest 54 | %td= r.size 55 | %td 56 | = link_to 'show', replacement_path(r) 57 | = link_to 'download', replacement_path(r, download: true) 58 | -------------------------------------------------------------------------------- /app/models/flag.rb: -------------------------------------------------------------------------------- 1 | class Flag < ActiveRecord::Base 2 | belongs_to :team 3 | belongs_to :service 4 | has_many :captures 5 | 6 | TOTAL_FLAGS = 10 * # service count 7 | 15 * # team count 8 | 1337 9 | 10 | def self.initial_distribution 11 | raise "Refusing to distribute with existing flags" unless Flag.count == 0 12 | 13 | transaction do 14 | teams = Team.without_legitbs.to_a 15 | services = Service.all 16 | 17 | tranche_count = teams.count * services.count 18 | tranche_size = Flag::TOTAL_FLAGS / (tranche_count) 19 | tranche_remainder = Flag::TOTAL_FLAGS % (tranche_count) 20 | 21 | unless tranche_remainder == 0 22 | puts "#{teams.count} teams * #{services.count} services = #{tranche_count} tranches" 23 | puts "#{Flag::TOTAL_FLAGS} / #{tranche_count} = #{Flag::TOTAL_FLAGS.to_f / tranche_count.to_f}" 24 | puts "add #{tranche_size - tranche_remainder} or remove #{tranche_remainder}" 25 | raise "had flags left over when planning allocation" 26 | end 27 | 28 | puts "#{services.count} services, #{teams.count} teams, #{tranche_size} flags per" 29 | 30 | teams.each do |t| 31 | print "flags for #{t.name}: " 32 | services.each do |s| 33 | tranche_size.times do 34 | Flag.create team: t, service: s 35 | end 36 | print '.' 37 | end 38 | 39 | puts 40 | end 41 | end 42 | end 43 | 44 | def self.collect_livectf_flags(livectf_services) 45 | livectf_services.each do |serv| 46 | instances = Instance.where service: serv 47 | legitbs_inst = instances.find_by team: Team.legitbs 48 | 49 | Flag.where(service: serv).update_all(team_id: Team.legitbs.id) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 8.2 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem 'pg' 16 | # 17 | development: 18 | adapter: postgresql 19 | encoding: unicode 20 | database: scorebot_development 21 | pool: 5 22 | 23 | # Connect on a TCP socket. Omitted by default since the client uses a 24 | # domain socket that doesn't need configuration. Windows does not have 25 | # domain sockets, so uncomment these lines. 26 | #host: localhost 27 | 28 | # The TCP port the server listens on. Defaults to 5432. 29 | # If your server runs on a different port number, change accordingly. 30 | #port: 5432 31 | 32 | # Schema search path. The server defaults to $user,public 33 | #schema_search_path: myapp,sharedapp,public 34 | 35 | # Minimum log levels, in increasing order: 36 | # debug5, debug4, debug3, debug2, debug1, 37 | # log, notice, warning, error, fatal, and panic 38 | # Defaults to warning. 39 | #min_messages: notice 40 | 41 | # Warning: The database defined as "test" will be erased and 42 | # re-generated from your development database when you run "rake". 43 | # Do not set this db to the same as development or production. 44 | test: 45 | adapter: postgresql 46 | encoding: unicode 47 | database: scorebot_test 48 | pool: 5 49 | 50 | production: 51 | adapter: postgresql 52 | host: db 53 | username: postgres 54 | database: scorebot_production 55 | pool: 100 56 | -------------------------------------------------------------------------------- /app/views/dashboard/index.html.haml: -------------------------------------------------------------------------------- 1 | .dashboard.scene 2 | .parcel.right 3 | = render partial: 'shared/timers' 4 | .status 5 | %h1 Service Status 6 | %table 7 | %thead 8 | %th service 9 | %th port 10 | %th -15m 11 | %th -10m 12 | %th -5m 13 | %tbody 14 | - @instances.each do |s| 15 | %tr 16 | %th&= s.service.name 17 | %td&= s.service.port 18 | - s.uptime_check.each do |c| 19 | - if c 20 | %td.ok OK 21 | - else 22 | %td.down DOWN 23 | 24 | .parcel.left 25 | #replace 26 | %h1 Replace 27 | %ul 28 | %li= link_to 'Replace a service binary', new_replacement_path 29 | %li= link_to 'Get service binaries', replacements_path 30 | #redeem 31 | %h1 Redeem 32 | %p 33 | You really should automate this. 34 | = link_to 'Read the "How to Play" screen.', howto_path 35 | = form_tag redemption_path, method: :post, target: 'redeem_target', id: 'redemption_form' do 36 | - 2.times do 37 | %p 38 | = text_field_tag 'tokens[]', '', placeholder: 'token' 39 | %p 40 | = text_field_tag 'tokens[]', '', placeholder: 'token' 41 | = submit_tag 'punch it' 42 | %iframe{id: 'redeem_target', name: 'redeem_target', style: 'display: none'} 43 | #tickets 44 | %h1 Trouble Tickets 45 | = form_for current_team.tickets.new do |f| 46 | %p 47 | = f.text_area :body, placeholder: "Problems? Securely send a message to Legitimate Business Syndicate. Definitely paste any tokens or UUIDs from redemptions." 48 | %nav 49 | %ul 50 | %li= f.submit 'Submit ticket' 51 | %li= link_to 'List my tickets', tickets_path 52 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # config valid only for Capistrano 3.1 2 | lock '3.4.0' 3 | 4 | set :application, 'scorebot' 5 | set :repo_url, 'git@waitingf.org:scorebot.git' 6 | #set :repo_url, 'file:///home/scorebot/scorebot-repo.git' 7 | 8 | # Default branch is :master 9 | # ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call 10 | 11 | # Default deploy_to directory is /var/www/my_app 12 | # set :deploy_to, '/home/scorebot/production' 13 | 14 | # Default value for :scm is :git 15 | # set :scm, :git 16 | 17 | # Default value for :format is :pretty 18 | # set :format, :pretty 19 | 20 | # Default value for :log_level is :debug 21 | # set :log_level, :debug 22 | 23 | # Default value for :pty is false 24 | # set :pty, true 25 | 26 | # Default value for :linked_files is [] 27 | # set :linked_files, %w{config/database.yml} 28 | 29 | # Default value for linked_dirs is [] 30 | # set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system} 31 | 32 | set :linked_dirs, fetch(:linked_dirs) + %w{tmp/pids tmp/logs logs tmp/dumps} 33 | 34 | # Default value for default_env is {} 35 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } 36 | 37 | # Default value for keep_releases is 5 38 | # set :keep_releases, 5 39 | 40 | namespace :deploy do 41 | 42 | desc 'Restart application' 43 | task :restart do 44 | on roles(:app), in: :sequence, wait: 5 do 45 | within current_path do 46 | %w{puma service}.each do |f| 47 | pidfile = "#{current_path}/tmp/pids/#{f}.pid" 48 | next unless test("[ -f #{pidfile} ]") 49 | execute "kill `cat #{pidfile}`" 50 | end 51 | end 52 | end 53 | end 54 | 55 | after :publishing, :restart 56 | 57 | after :restart, :clear_cache do 58 | on roles(:web), in: :groups, limit: 3, wait: 10 do 59 | # Here we can do anything such as: 60 | # within release_path do 61 | # execute :rake, 'cache:clear' 62 | # end 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /app/controllers/tickets_controller.rb: -------------------------------------------------------------------------------- 1 | class TicketsController < ApplicationController 2 | before_action :set_ticket, only: [:show, :edit, :update, :destroy, :resolve] 3 | before_action :require_legitbs, only: %i{ resolve } 4 | 5 | # GET /tickets 6 | def index 7 | @title = "All Tickets" 8 | @tickets = ticket_scope.order(created_at: :desc) 9 | if params[:scope] != 'all' 10 | @title = 'Unresolved Tickets' 11 | @tickets = @tickets.unresolved 12 | end 13 | 14 | if is_legitbs? 15 | @title += " from Everyone" 16 | end 17 | end 18 | 19 | # GET /tickets/1 20 | def show 21 | end 22 | 23 | # GET /tickets/new 24 | def new 25 | @ticket = current_team.tickets.new 26 | end 27 | 28 | # GET /tickets/1/edit 29 | def edit 30 | end 31 | 32 | # POST /tickets 33 | def create 34 | @ticket = current_team.tickets.new(ticket_params) 35 | 36 | if @ticket.save 37 | Event.new('ticket', r.as_json).publish! rescue nil 38 | redirect_to @ticket, notice: 'Ticket was successfully created.' 39 | else 40 | render :new 41 | end 42 | end 43 | 44 | # PATCH/PUT /tickets/1 45 | def update 46 | if @ticket.update(ticket_params) 47 | redirect_to @ticket, notice: 'Ticket was successfully updated.' 48 | else 49 | render :edit 50 | end 51 | end 52 | 53 | def resolve 54 | @ticket.resolve! 55 | redirect_to tickets_path 56 | end 57 | 58 | def unresolve 59 | @ticket.unresolve! 60 | redirect_to tickets_path 61 | end 62 | 63 | private 64 | # Use callbacks to share common setup or constraints between actions. 65 | def set_ticket 66 | @ticket = ticket_scope.find(params[:id]) 67 | end 68 | 69 | def ticket_scope 70 | if is_legitbs? 71 | Ticket 72 | else 73 | current_team.tickets 74 | end 75 | end 76 | 77 | # Only allow a trusted parameter "white list" through. 78 | def ticket_params 79 | params.require(:ticket).permit(:body) 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/models/availability_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AvailabilityTest < ActiveSupport::TestCase 4 | should belong_to :instance 5 | should belong_to :round 6 | 7 | context 'with a round, an instance, and a shell process' do 8 | setup do 9 | @instance = FactoryGirl.create :instance 10 | 11 | @shell = mock('shell') 12 | ShellProcess. 13 | expects(:new). 14 | with(Rails.root.join('scripts', @instance.service.name), 15 | 'availability', 16 | is_a(Integer)). 17 | returns(@shell) 18 | 19 | @token = FactoryGirl.create :token, instance: @instance 20 | @dingus = [rand(2**64).to_s(16)].pack('H*') 21 | 22 | @memo = <<-EOF 23 | example memo 24 | !!legitbs-validate-token-7OPuwAj #{@token.to_token_string} 25 | EOF 26 | 27 | @shell.expects(:status).returns(0) 28 | @shell.expects(:output).at_least(1).returns(@memo) 29 | end 30 | 31 | should 'create an availability from an instance' do 32 | @availability = Availability.check @instance, @round 33 | 34 | assert @availability 35 | assert_equal 0, @availability.status 36 | assert_equal @memo, @availability.memo 37 | 38 | assert_equal @token, @availability.token 39 | end 40 | end 41 | 42 | should 'distribute flags' do 43 | @instance = FactoryGirl.create :instance 44 | @lbs_instance = FactoryGirl.create :lbs_instance, service: @instance.service 45 | 46 | @flags = FactoryGirl.create_list :flag, 19, team: @instance.team, service: @instance.service 47 | @teams = FactoryGirl.create_list :team, 19 48 | 49 | @availability = FactoryGirl.create :down_availability, instance: @instance 50 | @lbs_availability = FactoryGirl.create(:availability, 51 | instance: @lbs_instance, 52 | round: @availability.round) 53 | 54 | @availability.process_movements(@availability.round) 55 | 56 | assert @flags.any?{|f| f.reload.team != @instance.team} 57 | end 58 | 59 | should 'log flag distribution' 60 | end 61 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Debug mode disables concatenation and preprocessing of assets. 41 | # This option may cause significant delays in view rendering with a large 42 | # number of complex assets. 43 | config.assets.debug = true 44 | 45 | # Suppress logger output for asset requests. 46 | config.assets.quiet = true 47 | 48 | # Raises error for missing translations 49 | # config.action_view.raise_on_missing_translations = true 50 | 51 | # Use an evented file watcher to asynchronously detect changes in source code, 52 | # routes, locales, etc. This feature depends on the listen gem. 53 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 54 | end 55 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 4 | gem 'rails', '~> 5.1.1' 5 | 6 | gem 'pry-rails' 7 | gem 'base62' 8 | gem 'pngqr' 9 | 10 | gem 'fog-aws' 11 | 12 | # Use postgresql as the database for Active Record 13 | gem 'pg' 14 | gem 'immigrant' 15 | 16 | gem 'redis' 17 | 18 | # Use SCSS for stylesheets 19 | gem 'sass-rails' 20 | gem 'haml-rails' 21 | gem 'bourbon' 22 | gem 'rdiscount' 23 | 24 | gem 'listen' 25 | 26 | gem 'cocaine' 27 | 28 | # Use Uglifier as compressor for JavaScript assets 29 | gem 'uglifier' #, '>= 1.3.0' 30 | 31 | # Use CoffeeScript for .js.coffee assets and views 32 | gem 'coffee-rails' #, '~> 4.0' 33 | 34 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 35 | # gem 'therubyracer', platforms: :ruby 36 | 37 | # Use jquery as the JavaScript library 38 | gem 'jquery-rails' 39 | 40 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 41 | # gem 'turbolinks' 42 | 43 | gem 'high_voltage' 44 | 45 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 46 | # gem 'jbuilder', '~> 1.2' 47 | 48 | gem 'celluloid', require: false 49 | 50 | 51 | gem 'rack-mini-profiler' 52 | gem 'flamegraph' 53 | gem 'stackprof' # ruby 2.1+ only 54 | gem 'memory_profiler' 55 | gem 'statsd-instrument' 56 | 57 | group :doc do 58 | # bundle exec rake doc:rails generates the API under doc/api. 59 | gem 'sdoc', require: false 60 | end 61 | 62 | group :development do 63 | gem 'rails-erd' 64 | gem 'better_errors' 65 | gem 'binding_of_caller' 66 | end 67 | 68 | group :production do 69 | gem 'therubyracer' 70 | gem 'puma' 71 | end 72 | 73 | group :development, :test do 74 | gem 'shoulda' 75 | gem 'factory_girl_rails' 76 | gem 'mocha', require: false 77 | end 78 | 79 | # Use ActiveModel has_secure_password 80 | gem 'bcrypt-ruby', '~> 3.0', require: 'bcrypt' 81 | 82 | # Use unicorn as the app server 83 | # gem 'unicorn' 84 | 85 | # Use Capistrano for deployment 86 | gem 'capistrano-rails', group: :development 87 | 88 | # Use debugger 89 | # gem 'debugger', group: [:development, :test] 90 | -------------------------------------------------------------------------------- /app/assets/javascripts/timers.js.coffee: -------------------------------------------------------------------------------- 1 | jQuery ($) -> 2 | return unless $('#timers').length == 1 3 | 4 | pad = (n) -> 5 | return "0#{n}" if n < 10 & n >= 0 6 | n 7 | 8 | class Countdown 9 | constructor: -> 10 | @timers = $('#timers') 11 | @timerPath = @timers.data('timer-path') 12 | @interval = @timers.data('timer-interval') 13 | @template = @timers.html() 14 | @timers.html('') 15 | @startPoll() 16 | startPoll: -> 17 | window.setTimeout(@poll(), 0) 18 | requeue: -> 19 | finish = new XDate() 20 | lag = @start.diffMilliseconds(finish) 21 | window.setTimeout(@poll(), @interval - lag) 22 | poll: -> 23 | return => 24 | @start = new XDate() 25 | if !@recentTimers? || (@start > @recentTimers.next) 26 | $.ajax 27 | url: @timerPath 28 | dataType: 'json' 29 | method: 'get' 30 | success: @receive() 31 | else 32 | @lazyUpdate() 33 | receive: -> 34 | return (data, textStatus, jqx) => 35 | server = new XDate data['time'] 36 | @adjustment = @start.diffMilliseconds(server) 37 | @recentTimers = data.timers 38 | @recentTimers.next = @start.clone().addSeconds(15) 39 | @lazyUpdate() 40 | lazyUpdate: -> 41 | server = @start.addMilliseconds(@adjustment) 42 | game = @toRemnant(server, new XDate @recentTimers.game) 43 | today = @toRemnant(server, new XDate @recentTimers.today) 44 | round = @toRemnant(server, new XDate @recentTimers.round) 45 | h = Mustache.render @template, 46 | game: game 47 | today: today 48 | round: round 49 | @timers.html h 50 | 51 | @requeue() 52 | toRemnant: (now, ending) -> 53 | try 54 | diff = 55 | s: pad(Math.abs(Math.floor(now.diffSeconds(ending) % 60))) 56 | m: pad(Math.abs(Math.floor(now.diffMinutes(ending) % 60))) 57 | h: pad(Math.floor(now.diffHours(ending))) 58 | next: now.diffMilliseconds(ending) 59 | ending: ending 60 | return diff 61 | catch 62 | return {h: '00', m: '00', s: '00', next: 0, ending: now} 63 | 64 | Countdown.countdown = new Countdown 65 | -------------------------------------------------------------------------------- /app/views/admin/teams/show.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Team: #{@team.name} 2 | 3 | %table 4 | %tr 5 | %th name 6 | %td= @team.name 7 | %tr 8 | %th display name 9 | %td= @team.display 10 | %tr 11 | %th certname 12 | %td= @team.certname 13 | %tr 14 | %th address 15 | %td= @team.address 16 | %tr 17 | %th uuid 18 | %td= @team.uuid 19 | 20 | %h2 flags 21 | 22 | %table 23 | - @team.instances.joins(:service).sort_by{|i|i.service.name.downcase}.each do |inst| 24 | %tr 25 | %th= inst.service.name 26 | %td= inst.flags.count 27 | 28 | %h2 failed redemptions 29 | 30 | %table 31 | %tbody 32 | %tr 33 | %th dupe 34 | %td= @team.dupe_ctr 35 | %tr 36 | %th old 37 | %td= @team.old_ctr 38 | %tr 39 | %th notfound 40 | %td= @team.notfound_ctr 41 | %tr 42 | %th self 43 | %td= @team.self_ctr 44 | %tr 45 | %th other 46 | %td= @team.other_ctr 47 | 48 | %h2 successful redemptions 49 | 50 | %table 51 | %thead 52 | %tr 53 | %th id 54 | %th instance 55 | %th round 56 | %th token 57 | %th captures 58 | %tbody 59 | - @team.redemptions.order(id: :desc).limit(@round_limit * 30).each do |r| 60 | %tr 61 | %td= link_to r.id, admin_redemption_path(r.id) 62 | %td= link_to r.token.instance.id, admin_instance_path(r.token.instance_id) 63 | %td= link_to r.round_id, admin_round_path(r.round_id) 64 | %td= link_to r.token.to_fake_string, admin_token_path(r.token_id) 65 | %td= r.captures.count 66 | 67 | 68 | %h2 service availability 69 | 70 | %p 71 | Showing #{@round_limit} rounds. 72 | = link_to "Show all?", limit: 1_000 73 | 74 | %table#availabilities 75 | %thead 76 | %tr 77 | %th round id 78 | - @services.each do |s| 79 | %th= s.name 80 | %tbody 81 | - @rounds.each do |r| 82 | %tr 83 | %th 84 | = r.id 85 | = r.created_at.to_formatted_s(:ctf) 86 | - @services.each do |s| 87 | - i = @team.instances.find_by(service: s) 88 | - av = i.availabilities.find_by(round: r) 89 | - if av.nil? 90 | %td 91 | - else 92 | %td{class: av.healthy?.inspect} 93 | = av.healthy? 94 | -------------------------------------------------------------------------------- /app/assets/stylesheets/scoreboard.sass: -------------------------------------------------------------------------------- 1 | body.scoreboard 2 | .banner 3 | display: none 4 | .profiler-left 5 | display: none 6 | .scoreboard.scene 7 | .scoreboard-holder 8 | table 9 | $alpha_adjustment: -.15 10 | float: left 11 | margin-top: 1em 12 | background-image: asset_url('crinkled-paper-1x.jpg') 13 | color: $superdark_grape 14 | tbody tr:nth-child(odd) 15 | background-color: rgba(0, 0, 0, 0.25) 16 | td, th.team_name 17 | text-align: right 18 | padding-right: .5em 19 | th 20 | text-align: left 21 | tbody th 22 | text-align: right 23 | width: 14em 24 | font-size: 1em 25 | padding-right: .5em 26 | td 27 | text-align: right 28 | &.flags 29 | width: 700px 30 | .points 31 | z-index: 1 32 | position: relative 33 | width: 4em 34 | color: $light_amber 35 | background-color: $dark_grape 36 | padding: 2px 37 | font-weight: bold 38 | &.left 39 | float: left 40 | .bar 41 | z-index: 0 42 | position: relative 43 | background-color: $dark_grape 44 | padding: 2px 45 | margin-left: -1px 46 | &.left 47 | float: left 48 | 49 | #timers 50 | position: fixed 51 | top: 2em 52 | right: 2em 53 | z-index: 10 54 | background-color: $dark_amber 55 | background-image: asset_url('crinkled-paper-1x.jpg') 56 | text-align: center 57 | border: 0px 58 | border-radius: 5px 59 | box-shadow: 0px 4px 16px 2px black 60 | h1 61 | display: none 62 | .desc 63 | @include permie 64 | color: black 65 | .timer 66 | padding: 2px 67 | @include mono 68 | 69 | @media screen and (max-width: 1281px) 70 | .scoreboard.scene 71 | font-size: 1.4em 72 | width: 1280px 73 | .scoreboard-holder table 74 | margin: 0 75 | margin-left: -2em 76 | 77 | @media screen and (min-width: 1400px) 78 | .scoreboard.scene 79 | font-size: 1.45em 80 | width: 1200px 81 | 82 | @media screen and (max-width: 840px) 83 | .scoreboard.scene 84 | width: 600px 85 | font-size: 0.82em 86 | -------------------------------------------------------------------------------- /test/models/round_finalizer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class RoundFinalizerTest < ActiveSupport::TestCase 4 | setup do 5 | @old_rounds = Token::EXPIRATION.times.map { FactoryGirl.create :round } 6 | @token = FactoryGirl.create :token, round: @old_rounds.first 7 | 8 | @redemption = FactoryGirl.create :redemption, token: @token 9 | @round = @redemption.round 10 | @instance = @token.instance 11 | @service = @instance.service 12 | 13 | @lbs_instance = FactoryGirl.create :lbs_instance, service: @service 14 | end 15 | 16 | should 'be creatable with a round and service' do 17 | RoundFinalizer.new @round, @service 18 | end 19 | 20 | context 'with redemptions and failed availabilities' do 21 | setup do 22 | @owned_team = @instance.team 23 | @redeeming_team = @redemption.team 24 | @idle_team = FactoryGirl.create :team 25 | 26 | @lbs_availability = FactoryGirl.create :availability, instance: @lbs_instance, round: @round 27 | @failed_availability = FactoryGirl.create :down_availability, instance: @instance, round: @round 28 | 29 | @finalizer = RoundFinalizer.new @round, @service 30 | end 31 | 32 | should 'identify candidate tokens and failed availabilities' do 33 | assert_includes @finalizer.candidates, @token 34 | assert_includes @finalizer.candidates, @failed_availability 35 | end 36 | 37 | should 'scramble scoring events consistently' do 38 | assert_equal @finalizer.candidates, RoundFinalizer.new(@round, @service).candidates 39 | end 40 | 41 | should 'include the seed and sequence in the metadata' do 42 | assert_includes @finalizer.as_metadata_json, :seed 43 | assert_includes @finalizer.as_metadata_json, :sequence 44 | end 45 | 46 | should 'score events in sequence' do 47 | seq = sequence 'scoring' 48 | @finalizer.candidates.each do |c| 49 | c.expects(:process_movements).once.in_sequence(seq) 50 | c.expects(:as_movement_json).once.in_sequence(seq).returns(:ok) 51 | end 52 | 53 | @finalizer.movements 54 | end 55 | 56 | should 'have sensible movement data' do 57 | assert_kind_of Array, @finalizer.movements 58 | @finalizer.movements.each do |m| 59 | assert_kind_of Hash, m 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /app/controllers/replacements_controller.rb: -------------------------------------------------------------------------------- 1 | class ReplacementsController < ApplicationController 2 | def index 3 | @services = Service.enabled.all 4 | @teams = Team.order(certname: :asc).all 5 | 6 | @filters = {} 7 | @filters[:team_id] = params[:team_id].to_i if params[:team_id] 8 | @filters[:service_id] = Service.enabled.find(params[:service_id]).id if params[:service_id] 9 | 10 | per_page = 50 11 | 12 | @page = (params[:page] || 1).to_i 13 | return redirect_to(replacements_path(@filters)) if @page < 1 14 | @skip = (@page - 1) * per_page 15 | 16 | @replacements = Replacement. 17 | where(@filters). 18 | limit(50). 19 | offset(@skip). 20 | order(round_id: :desc, created_at: :desc). 21 | joins(:team, :service). 22 | where(services: { enabled: true }). 23 | all 24 | end 25 | 26 | def show 27 | @replacement = Replacement.find(params[:id]) 28 | unless @replacement.service.enabled 29 | raise ActiveRecord::RecordNotFound 30 | end 31 | if params[:download] 32 | return send_file @replacement.archive_path 33 | end 34 | end 35 | 36 | def new 37 | @services = Service.enabled.all 38 | @replacement = current_team.replacements.new 39 | end 40 | 41 | def create 42 | @service = Service.enabled.find(replacement_params[:service_id]) 43 | @services = Service.enabled.all 44 | 45 | @replacement = current_team.replacements. 46 | new(file: replacement_params[:file], 47 | service: @service, 48 | round: Round.current) 49 | begin 50 | if @replacement.save 51 | return redirect_to replacement_path(@replacement.id), 52 | notice: "Replaced #{@service.name}" 53 | end 54 | rescue PG::UniqueViolation => e 55 | @replacement.errors.add :base, "already replaced this round" 56 | rescue => e 57 | @replacement.errors.add :base, "problem dog: #{e.message}" 58 | logger.info e.backtrace 59 | end 60 | 61 | render action: 'new' 62 | end 63 | 64 | private 65 | def replacement_params 66 | params. 67 | require(:replacement). 68 | permit(:service_id, :file) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /bin/state_monitor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'redis' 4 | require 'json' 5 | require 'blink1' 6 | require 'celluloid/autostart' 7 | 8 | $redis = Redis.new(host: '10.3.1.8', 9 | port: 6379, 10 | password: 'vueshosBevounWiedMontowmuzDoHondafDeyWor' 11 | ) 12 | 13 | $blink = Blink1.new 14 | $blink.open 15 | $blink.off 16 | 17 | class StateSubscriber 18 | include Celluloid 19 | def initialize(monitor) 20 | @monitor = monitor 21 | end 22 | 23 | def subscribe 24 | msg = $redis.get 'scorebot_service_current_state_production' 25 | puts "loading initial state #{msg.length} bytes" 26 | @monitor.update_state msg 27 | 28 | $redis.subscribe('scorebot_service_state_production') do |on| 29 | on.message do |channel, msg| 30 | puts "received new state #{msg.length} bytes" 31 | @monitor.async.update_state(msg) 32 | end 33 | end 34 | end 35 | end 36 | 37 | class StateMonitor 38 | include Celluloid 39 | 40 | attr_accessor :name, :body, :published_at 41 | 42 | def initialize 43 | @lock = Mutex.new 44 | end 45 | 46 | def reschedule 47 | puts "rescheduling" 48 | @state_checker = every(2){ check_state } 49 | end 50 | 51 | def update_state(message) 52 | puts "updating state" 53 | data = JSON.parse message 54 | 55 | @lock.synchronize do 56 | self.name = data['state_name'] 57 | self.body = data['state_body'] 58 | self.published_at = Time.at data['published_at'] 59 | end 60 | puts "updated state" 61 | end 62 | 63 | def check_state 64 | puts "checking state" 65 | @lock.synchronize do 66 | delta = Time.now - self.published_at 67 | puts "name: #{name}" 68 | puts "since last publish: #{delta}" 69 | 70 | if delta < 10 71 | $blink.off 72 | return 73 | end 74 | 75 | if name == 'commencing' && delta > 30 76 | $blink.set_rgb(255, 0, 255) 77 | elsif name == 'scheduling' && delta > 100 78 | $blink.set_rgb(255, 0, 0) 79 | elsif name == 'waiting_to_end' && delta > 100 80 | $blink.set_rgb(255, 0, 0) 81 | end 82 | end 83 | puts "checked state" 84 | end 85 | end 86 | 87 | @monitor = StateMonitor.new 88 | @subscriber = StateSubscriber.new @monitor 89 | 90 | @monitor.reschedule 91 | puts "subscribing" 92 | @subscriber.async.subscribe 93 | 94 | sleep 95 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # If you are preloading your application and using Active Record, it's 36 | # recommended that you close any connections to the database before workers 37 | # are forked to prevent connection leakage. 38 | # 39 | # before_fork do 40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) 41 | # end 42 | 43 | # The code in the `on_worker_boot` will be called if you are using 44 | # clustered mode by specifying a number of `workers`. After each worker 45 | # process is booted, this block will be run. If you are using the `preload_app!` 46 | # option, you will want to use this block to reconnect to any threads 47 | # or connections that may have been created at application boot, as Ruby 48 | # cannot share connections between processes. 49 | # 50 | # on_worker_boot do 51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 52 | # end 53 | # 54 | 55 | # Allow puma to be restarted by `rails restart` command. 56 | plugin :tmp_restart 57 | -------------------------------------------------------------------------------- /app/views/admin/rounds/show.html.haml: -------------------------------------------------------------------------------- 1 | %h1 Round #{@round.id} 2 | 3 | %table 4 | %tbody 5 | %tr 6 | %th id 7 | %td= @round.id 8 | %tr 9 | %th signature 10 | %td= @round.signature 11 | %tr 12 | %th started 13 | %td= time_ago @round.created_at 14 | %tr 15 | %th ended 16 | %td 17 | - if @round.ended_at 18 | = time_ago @round.ended_at 19 | - else 20 | %em not yet 21 | %tr 22 | %th round length 23 | %td 24 | - if @round.ended_at 25 | = @round.ended_at - @round.created_at 26 | - else 27 | %em not yet 28 | 29 | %h2 Tokens / Deposits 30 | 31 | %table 32 | %thead 33 | %tr 34 | %th id 35 | %th instance 36 | %th status 37 | %th redemptions 38 | %th activities 39 | %tbody 40 | - @round.tokens.order(instance_id: :asc).each do |t| 41 | %tr 42 | %td= link_to t.id, admin_token_path(t) 43 | %td= describe_instance t.instance 44 | %td 45 | - if t.status == 0 46 | ok 47 | - else 48 | failed 49 | = t.status 50 | %td= t.redemptions.count 51 | %td 52 | = link_to 'show', admin_token_path(t) 53 | 54 | %h2 Redemptions 55 | 56 | %table 57 | %thead 58 | %tr 59 | %th id 60 | %th team 61 | %th instance 62 | %th when 63 | %th activities 64 | %tbody 65 | - @round.redemptions.order(created_at: :asc).each do |r| 66 | %tr 67 | %td= link_to r.id, admin_redemption_path(r) 68 | %td= link_to r.team.certname, admin_team_path(r.team) 69 | %td= describe_instance r.token.instance 70 | %td{title: r.created_at} 71 | = time_ago r.created_at 72 | %td 73 | = link_to 'show', admin_redemption_path(r) 74 | 75 | %h2 Availability Checks 76 | 77 | %table 78 | %thead 79 | %tr 80 | %th id 81 | %th instance 82 | %th when 83 | %th healthy? 84 | %th activities 85 | %tbody 86 | - @round.availabilities.order(created_at: :asc).each do |a| 87 | %tr 88 | %td= link_to a.id, admin_availability_path(a) 89 | %td= describe_instance a.instance 90 | %td{title: a.created_at} 91 | = time_ago_in_words a.created_at, include_seconds: true 92 | ago 93 | %td= a.healthy? 94 | %td 95 | = link_to 'show', admin_availability_path(a) 96 | 97 | %h2 Distribution 98 | 99 | - if @round.distribution 100 | %pre 101 | = @round.distribution.pretty_inspect 102 | -------------------------------------------------------------------------------- /test/models/token_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class TokenTest < ActiveSupport::TestCase 4 | should belong_to :instance 5 | should belong_to :round 6 | 7 | should have_many :redemptions 8 | 9 | should validate_presence_of :instance 10 | should validate_presence_of :round 11 | 12 | context "token generation" do 13 | should "generate and validate a token string" do 14 | token = FactoryGirl.create :token 15 | token_str = token.to_token_string 16 | 17 | token2 = Token.from_token_string token_str 18 | 19 | assert_equal token, token2 20 | 21 | token.destroy 22 | end 23 | 24 | should "resist creating multiple tokens per instance-round" do 25 | token = FactoryGirl.create :token 26 | 27 | assert_uniqueness_constraint do 28 | token2 = Token.create token.attributes 29 | end 30 | end 31 | end 32 | 33 | context 'token deposition' do 34 | setup do 35 | @instance = FactoryGirl.create :instance 36 | @round = FactoryGirl.create :round 37 | end 38 | should 'run a per-service deposit script' do 39 | @shell = mock('shell', output: 'example', status: 0) 40 | 41 | ShellProcess. 42 | expects(:new). 43 | with(Rails.root.join('scripts', @instance.service.name), 44 | 'deposit', 45 | @instance.team_id, 46 | is_a(String), 47 | @round.id). 48 | returns(@shell) 49 | 50 | @token = Token.create instance: @instance, round: @round 51 | @token.deposit 52 | 53 | assert_equal @token, Token.from_token_string(@token.to_token_string) 54 | end 55 | should 'allow deposit scripts to substitute tokens' do 56 | replaced_token = "replaced-token-#{rand(36**5).to_s(36)}" 57 | @shell = mock('shell', output: "!!legitbs-replace-token-3ELrtvi #{replaced_token}", status: 0) 58 | 59 | ShellProcess. 60 | expects(:new). 61 | with(Rails.root.join('scripts', @instance.service.name), 62 | 'deposit', 63 | @instance.team_id, 64 | is_a(String), 65 | @round.id). 66 | returns(@shell) 67 | 68 | @token = Token.create instance: @instance, round: @round 69 | @token.deposit 70 | @token.save 71 | 72 | assert_equal @token, Token.from_token_string(replaced_token) 73 | end 74 | end 75 | 76 | should "not be eligible after #{Token::EXPIRATION} rounds" do 77 | token = FactoryGirl.create :token 78 | assert token.eligible? 79 | 80 | Token::EXPIRATION.times do 81 | FactoryGirl.create :round 82 | assert token.eligible? 83 | end 84 | 85 | refute_includes Token.expiring, token 86 | 87 | FactoryGirl.create :round 88 | refute token.eligible? 89 | 90 | assert_includes Token.expiring, token 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /app/controllers/admin/replacements_controller.rb: -------------------------------------------------------------------------------- 1 | class Admin::ReplacementsController < Admin::BaseController 2 | before_action :set_replacement, only: [:show, :edit, :update, :destroy] 3 | 4 | # GET /replacements 5 | def index 6 | @replacements = Replacement.order(round_id: :asc, id: :asc).all 7 | end 8 | 9 | # GET /replacements/1 10 | def show 11 | @sames = Replacement. 12 | where(digest: @replacement.digest). 13 | order(created_at: :asc). 14 | all 15 | 16 | @teams = Replacement. 17 | where(team_id: @replacement.team_id, 18 | service: @replacement.service). 19 | order(created_at: :asc). 20 | all 21 | 22 | @next = Replacement. 23 | where(team_id: @replacement.team_id, 24 | service: @replacement.service). 25 | where('id > ?', @replacement.id). 26 | order(id: :asc). 27 | first 28 | 29 | @instance = Instance. 30 | where(team: @replacement.team, 31 | service: @replacement.service). 32 | first 33 | 34 | @tokens = @instance. 35 | tokens. 36 | where('created_at > ?', 37 | @replacement.created_at) 38 | 39 | if @next 40 | @tokens = @tokens. 41 | where('created_at < ?', 42 | @next.created_at) 43 | end 44 | 45 | @redemptions = @tokens.map(&:redemptions).flatten 46 | end 47 | 48 | # GET /replacements/new 49 | def new 50 | @replacement = Replacement.new(round_id: 1, team_id: 16) 51 | end 52 | 53 | # GET /replacements/1/edit 54 | def edit 55 | end 56 | 57 | # POST /replacements 58 | def create 59 | @replacement = Replacement.new(replacement_params) 60 | 61 | if @replacement.save 62 | redirect_to @replacement, notice: 'Replacement was successfully created.' 63 | else 64 | render :new 65 | end 66 | end 67 | 68 | # PATCH/PUT /replacements/1 69 | def update 70 | if @replacement.update(replacement_params) 71 | redirect_to @replacement, notice: 'Replacement was successfully updated.' 72 | else 73 | render :edit 74 | end 75 | end 76 | 77 | # DELETE /replacements/1 78 | def destroy 79 | @replacement.destroy 80 | redirect_to replacements_url, notice: 'Replacement was successfully destroyed.' 81 | end 82 | 83 | private 84 | # Use callbacks to share common setup or constraints between actions. 85 | def set_replacement 86 | @replacement = Replacement.find(params[:id]) 87 | end 88 | 89 | # Only allow a trusted parameter "white list" through. 90 | def replacement_params 91 | params.require(:replacement).permit(:team_id, :service_id, :round_id, :file) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /app/models/team.rb: -------------------------------------------------------------------------------- 1 | class Team < ActiveRecord::Base 2 | has_many :redemptions 3 | has_many :flags 4 | has_many :captures, through: :redemptions 5 | has_many :instances 6 | has_many :tickets 7 | has_many :replacements 8 | before_create :set_uuid 9 | 10 | after_rollback :flush_counters 11 | attr_accessor :dupe_ctr_defer 12 | attr_accessor :other_ctr_defer 13 | 14 | PARTICIPANT_COUNT = 15 15 | 16 | def as_ca_json 17 | { 18 | teamname: name, 19 | uuid: uuid, 20 | certname: certname 21 | } 22 | end 23 | 24 | def self.legitbs 25 | return @@legitbs if defined?(@@legitbs) && (@@legitbs.reload rescue false) 26 | @@legitbs = find_by uuid: 'deadbeef-7872-499a-a060-3143de953e28' 27 | end 28 | 29 | def self.without_legitbs 30 | where('certname != ?', 'legitbs') 31 | end 32 | 33 | def self.for_scoreboard 34 | q = <<-SQL 35 | SELECT 36 | t.name, 37 | t.id, 38 | count(f.id) as score, 39 | coalesce(t.display, t.name) as display_name 40 | FROM teams as t 41 | left JOIN flags AS f 42 | ON f.team_id = t.id 43 | WHERE t.certname != 'legitbs' 44 | GROUP BY t.name, t.id, display_name 45 | ORDER BY 46 | score desc, 47 | t.name asc 48 | SQL 49 | 50 | StatsD.measure('scoreboard_query'){ connection.select_all(q).map(&:symbolize_keys) } 51 | end 52 | 53 | def self.as_standings_json 54 | data = for_scoreboard 55 | 56 | place = 0 57 | prev_score = Flag.count + 1 58 | place_buf = 1 59 | { 60 | standings: data.map do |r| 61 | score = r[:score].to_i 62 | if score < prev_score 63 | place += place_buf 64 | place_buf = 1 65 | prev_score = score 66 | else 67 | place_buf += 1 68 | end 69 | { pos: place, team: r[:name], score: (16 - place), id: r[:id], place: place } 70 | # { pos: place, 71 | # team: r[:name], 72 | # score: score, 73 | # id: r[:id], 74 | # place: place } 75 | end, 76 | display_names: Hash[data.map do |r| 77 | [r[:id], r[:display_name]] 78 | end], 79 | generated_at: Time.now 80 | } 81 | end 82 | 83 | def scores_by_service 84 | q = <<-SQL 85 | SELECT 86 | service_id, count(service_id) 87 | FROM flags 88 | WHERE team_id=#{id} 89 | GROUP BY service_id 90 | ORDER BY service_id ASC 91 | SQL 92 | 93 | result = self.class.connection.select_all(q) 94 | Hash[result.map do |row| 95 | [row['service_id'].to_i, row['count'].to_i] 96 | end] 97 | end 98 | 99 | private 100 | def set_uuid 101 | self.uuid ||= SecureRandom.uuid 102 | end 103 | 104 | def flush_counters 105 | increment! :dupe_ctr, dupe_ctr_defer if dupe_ctr_defer 106 | increment! :other_ctr, other_ctr_defer if other_ctr_defer 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /app/models/redemption.rb: -------------------------------------------------------------------------------- 1 | class Redemption < ActiveRecord::Base 2 | belongs_to :team 3 | belongs_to :token, counter_cache: true 4 | belongs_to :round 5 | has_many :captures 6 | has_many :flags, through: :captures 7 | 8 | before_create :set_uuid 9 | after_create :log_spew 10 | 11 | def self.redeem_for(team, token_str) 12 | transaction do 13 | round = Round.current 14 | 15 | token = Token.from_token_string token_str 16 | if token.blank? 17 | team.increment! :notfound_ctr 18 | raise NoTokenError.new 19 | end 20 | unless token.eligible? 21 | team.increment! :old_ctr 22 | raise OldTokenError.new 23 | end 24 | if team == token.instance.team 25 | team.increment! :self_ctr 26 | raise SelfScoringError.new 27 | end 28 | 29 | begin 30 | candidate = create team: team, token: token, round: round 31 | rescue ActiveRecord::RecordNotUnique => e 32 | team.dupe_ctr_defer ||= 0 33 | team.dupe_ctr_defer += 1 34 | raise DuplicateTokenError.new 35 | rescue => e 36 | team.other_ctr_defer ||= 0 37 | team.other_ctr_defer += 1 38 | raise OtherTokenError.new e 39 | end 40 | 41 | return candidate 42 | end 43 | end 44 | 45 | class OtherTokenError < ArgumentError 46 | def initialize(e) 47 | correlation = SecureRandom.uuid 48 | Scorebot.log( 49 | [correlation, 50 | e.class.name, 51 | e.message, 52 | e.backtrace.join(', ')]. 53 | join('; ')) 54 | super "some other error #{correlation}" 55 | end 56 | end 57 | 58 | class DuplicateTokenError < ArgumentError 59 | def initialize 60 | super "Already redeemed that token" 61 | end 62 | end 63 | 64 | class OldTokenError < ArgumentError 65 | def initialize 66 | super "Token too old" 67 | end 68 | end 69 | 70 | class NoTokenError < ArgumentError 71 | def initialize 72 | super "Token doesn't exist" 73 | end 74 | end 75 | 76 | class SelfScoringError < ArgumentError 77 | def initialize 78 | super "Can't redeem your own tokens" 79 | end 80 | end 81 | 82 | def as_event_json 83 | { 84 | redeeming_team: team.as_json, 85 | owned_team: token.instance.team.as_json, 86 | service: token.instance.service.as_json, 87 | redemption_stats: { 88 | service: token.instance.service.total_redemptions, 89 | instance: token.instance.total_redemptions, 90 | redeeming_team: team.redemptions.count, 91 | all: Redemption.count, 92 | }, 93 | redeemed_at_local: created_at.to_formatted_s(:ctf) 94 | } 95 | end 96 | 97 | private 98 | def set_uuid 99 | self.uuid = SecureRandom.uuid 100 | end 101 | 102 | def log_spew 103 | Scorebot.log "Redeemed token #{token_id} from #{token.instance.team.name} #{token.instance.service.name} for team #{team.name} as #{uuid}" 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/models/availability_check_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class AvailabilityCheckTest < ActiveSupport::TestCase 4 | setup do 5 | @round = FactoryGirl.create :current_round 6 | 7 | @legitbs = FactoryGirl.create :legitbs 8 | Team.stubs(:legitbs).returns(@legitbs) 9 | @general_teams = FactoryGirl.create_list :team, 20 10 | 11 | @service = FactoryGirl.create :service, name: 'noop', enabled: true 12 | @shell = stub 'shell', status: 0, output: 'okay' 13 | ShellProcess.stubs(:new).returns(@shell) 14 | 15 | @lbs_instance = FactoryGirl.create :instance, team: @legitbs, service: @service 16 | @instances = @general_teams.map{|t| Instance.create team: t, service: @service } 17 | end 18 | 19 | 20 | context 'initializing' do 21 | should 'initialize for a service' do 22 | assert @check = AvailabilityCheck.for_service(@service) 23 | 24 | assert_equal @lbs_instance, @check.lbs_instance 25 | assert_equal @instances, @check.non_lbs_instances 26 | end 27 | 28 | should 'cache instances' do 29 | assert(checker = AvailabilityCheck.for_service(@service)) 30 | assert_equal checker.object_id, AvailabilityCheck.for_service(@service).object_id 31 | end 32 | end 33 | 34 | context 'timing' do 35 | setup do 36 | @check = AvailabilityCheck.for_service @service 37 | end 38 | 39 | should 'store timing history' do 40 | assert_respond_to @check, :timing_history 41 | assert_kind_of Enumerable, @check.timing_history 42 | assert_includes @check.timing_history, AvailabilityCheck::INITIAL_TIMING 43 | end 44 | 45 | should 'provide an average time' do 46 | assert_respond_to @check, :timing_average 47 | assert_kind_of Numeric, @check.timing_average 48 | @check.timing_history = [1, 2, 3] 49 | assert_equal 2.0, @check.timing_average 50 | end 51 | 52 | should 'warn if check will go longer than a round' do 53 | @check.timing_history = [ 2 * Round::ROUND_LENGTH ] 54 | assert @check.gonna_run_long? 55 | end 56 | 57 | should 'schedule checks for random times within the round' 58 | end 59 | 60 | context 'Availability checking' do 61 | should 'check all the teams' do 62 | lbs_av = stub 'legitbs availability', save: true 63 | @lbs_instance.expects(:check_availability).once.returns(lbs_av) 64 | 65 | @instances.each do |i| 66 | av = stub 'availability', status: 0 67 | i.expects(:check_availability).once.returns(av) 68 | av.expects(:save).once 69 | end 70 | 71 | @check = AvailabilityCheck.new @service 72 | 73 | @check.instance_variable_set(:@lbs_instance, @lbs_instance) 74 | @check.instance_variable_set(:@non_lbs_instances, @instances) 75 | 76 | assert_nothing_raised do 77 | @check.check_all_instances 78 | end 79 | end 80 | should 'claim flags on failures' do 81 | @check = AvailabilityCheck.new @service 82 | @check.instance_variable_set(:@lbs_check, 83 | stub('lbs availability', :healthy? => true)) 84 | 85 | failure = stub('failed', :healthy? => false) 86 | okay = stub('okay', :healthy? => true) 87 | checks = [failure] + [okay] * 19 88 | 89 | @check.instance_variable_set(:@non_lbs_checks, checks) 90 | 91 | failure.expects(:distribute!) 92 | 93 | assert_nothing_raised do 94 | @check.distribute_flags 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /app/models/availability_check.rb: -------------------------------------------------------------------------------- 1 | class AvailabilityCheck 2 | WORKERS = 16 3 | INITIAL_TIMING = 2 * 60 4 | 5 | attr_accessor :timing_history 6 | attr_accessor :deadline 7 | 8 | def self.for_service(service) 9 | @@cache ||= Hash.new 10 | @@cache[service] ||= new service 11 | end 12 | 13 | def initialize(service) 14 | @service = service 15 | self.timing_history = [INITIAL_TIMING] 16 | @history_lock = Mutex.new 17 | end 18 | 19 | def timing_average 20 | timing_history.sum.to_f / timing_history.length 21 | end 22 | 23 | def gonna_run_long? 24 | Time.now.to_f + timing_average >= deadline.to_f 25 | end 26 | 27 | def schedule! 28 | remaining = deadline.to_f - Time.now.to_f 29 | 30 | # skew earlier 31 | start_before = remaining - (70 + timing_average) 32 | 33 | wait = rand(start_before) 34 | 35 | @thread = Thread.new do 36 | chill wait 37 | clock = Time.now.to_f 38 | check_all_instances 39 | duration = Time.now.to_f - clock 40 | 41 | @history_lock.synchronize do 42 | timing_history << duration 43 | end 44 | 45 | Round.clear_active_connections! 46 | end 47 | 48 | wait 49 | end 50 | 51 | def join 52 | return unless defined? @thread 53 | Round.clear_active_connections! 54 | @thread.join 55 | end 56 | 57 | def instances 58 | @service.instances 59 | end 60 | 61 | def lbs_instance 62 | @lbs_instance ||= instances.where(team_id: Team.legitbs.id).first 63 | end 64 | 65 | def non_lbs_instances 66 | @non_lbs_instances ||= instances.where('team_id != ?', Team.legitbs.id).includes(:service, :team) 67 | end 68 | 69 | def check_all_instances 70 | return unless @service.enabled 71 | 72 | @lbs_check = lbs_instance.check_availability Round.current 73 | @lbs_check.save 74 | 75 | @non_lbs_checks = process_instance_queue 76 | end 77 | 78 | def process_instance_queue 79 | queue = non_lbs_instances.shuffle 80 | queue_mutex = Mutex.new 81 | 82 | results = [] 83 | result_mutex = Mutex.new 84 | 85 | round = Round.current 86 | 87 | threads = 1.upto(WORKERS).map do 88 | Thread.new do 89 | loop do 90 | instance = queue_mutex.synchronize do 91 | queue.shift 92 | end 93 | 94 | break if instance.nil? 95 | 96 | l "#{ instance.service.name } #{instance.team.certname} checking" 97 | 98 | av = instance.check_availability round 99 | l "#{ instance.service.name } #{instance.team.certname} status #{av.status}" 100 | result_mutex.synchronize do 101 | results.push av 102 | end 103 | end 104 | Round.clear_active_connections! 105 | end 106 | end 107 | 108 | threads.each {|t| t.join } 109 | 110 | Availability.transaction do 111 | results.each(&:save!) 112 | end 113 | 114 | results 115 | end 116 | 117 | def distribute_flags 118 | return unless @service.enabled 119 | return unless @lbs_check.healthy? 120 | 121 | @non_lbs_checks.reject(&:healthy?).each(&:distribute!) 122 | end 123 | 124 | private 125 | def chill(duration) 126 | schedule = Time.now.to_f + duration 127 | while Time.now.to_f < schedule 128 | sleep 0.1 129 | end 130 | end 131 | 132 | def l(str) 133 | Scorebot.log str 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /app/models/availability.rb: -------------------------------------------------------------------------------- 1 | class Availability < ActiveRecord::Base 2 | belongs_to :instance 3 | belongs_to :round 4 | belongs_to :token, optional: true 5 | scope :failed, -> { where.not(status: 0) } 6 | has_many :penalties 7 | 8 | def self.check(instance, round) 9 | candidate = new instance: instance, round: round 10 | candidate.check 11 | return candidate 12 | end 13 | 14 | def fix_availability 15 | return if status == 0 16 | self.status = 0 17 | self.memo = "administratively fixed by legitbs <3 vito@legitbs.net" 18 | save 19 | end 20 | 21 | def decoded_dingus 22 | return nil if dingus.nil? 23 | @decoded_dingus ||= Dingus.new self.dingus, plaintext: true 24 | end 25 | 26 | def legit_dingus? 27 | return nil if decoded_dingus.nil? 28 | return false unless decoded_dingus.legit? 29 | return false unless decoded_dingus.team == instance.team 30 | return false unless ((created_at.to_i - 30)..(created_at.to_i + 30)).cover? decoded_dingus.to_h[:clocktime] 31 | 32 | return true 33 | end 34 | 35 | def check 36 | service_name = instance.service.name 37 | team_address = instance.team.address 38 | 39 | dir = Rails.root.join 'scripts', service_name 40 | script = 'availability' 41 | 42 | shell = ShellProcess. 43 | new( 44 | dir, 45 | script, 46 | instance.team.id 47 | ) 48 | 49 | StatsD.measure "#{instance.team.certname}.#{instance.service.name}.availability" do 50 | self.status = shell.status 51 | end 52 | 53 | self.memo = shell.output.force_encoding('binary') 54 | 55 | load_dinguses shell.output 56 | 57 | if self.token_string and !self.token 58 | self.status = 420 59 | end 60 | 61 | self 62 | end 63 | 64 | def healthy? 65 | status == 0 66 | end 67 | 68 | def as_movement_json 69 | return { availability: { id: id, healthy: true } } if healthy? 70 | 71 | return { availability: as_json(only: :id, include: :penalties) } 72 | end 73 | 74 | def load_dinguses(memo) 75 | if has_token = /^!!legitbs-validate-token-7OPuwAj (.+)$/.match(memo) 76 | self.token_string = has_token[1] 77 | candidate_token = Token.from_token_string self.token_string 78 | 79 | return false if candidate_token.nil? 80 | return false if candidate_token.instance != self.instance 81 | return false if candidate_token.expired? 82 | 83 | self.token = candidate_token 84 | end 85 | end 86 | 87 | def process_movements(_round) 88 | return if instance.team == Team.legitbs 89 | return unless instance.legitbs_instance.availabilities.find_by(round: round).healthy? 90 | 91 | flags = instance.flags.limit(15) 92 | 93 | return distribute_parking(flags) if flags.count < 15 94 | return distribute_everywhere(flags) 95 | end 96 | 97 | private 98 | def distribute_everywhere(flags) 99 | teams = Team.where('id != ? and id != ?', 100 | Team.legitbs.id, 101 | instance.team.id).to_a 102 | 103 | Scorebot.log "reallocating #{flags.length} from #{instance.team.name} #{instance.service.name} flags to #{teams.map(&:certname)} teams" 104 | 105 | flags.zip(teams) do |f, t| 106 | break if t.nil? 107 | f.team = t 108 | f.save! 109 | penalties.create team_id: t.id, flag_id: f.id 110 | end 111 | end 112 | 113 | def distribute_parking(flags) 114 | flags.each do |f| 115 | f.team = Team.legitbs 116 | f.save! 117 | penalties.create team_id: Team.legitbs.id, flag_id: f.id 118 | end 119 | end 120 | end 121 | --------------------------------------------------------------------------------