mirror of
https://github.com/ENSL/ensl.org.git
synced 2024-11-15 01:11:23 +00:00
Merge pull request #18 from cblanc/cleanup_server
Attempt to remove RCON and clean up legacy code from Server Model
This commit is contained in:
commit
fcf8f2111d
25 changed files with 296 additions and 2535 deletions
1
Gemfile
1
Gemfile
|
@ -11,7 +11,6 @@ gem 'puma', '~> 2.11.1'
|
|||
gem 'exceptional', '~> 2.0.33'
|
||||
gem 'oj', '~> 2.5.5'
|
||||
gem 'faraday', '~> 0.9.0'
|
||||
gem 'gruff', '~> 0.3.6'
|
||||
gem 'nokogiri', '~> 1.6.1'
|
||||
gem 'bbcoder', '~> 1.0.1'
|
||||
gem 'sanitize', '~> 2.1.0'
|
||||
|
|
|
@ -119,7 +119,6 @@ GEM
|
|||
ffi (1.9.3)
|
||||
font-awesome-sass (4.1.0)
|
||||
sass (~> 3.2)
|
||||
gruff (0.3.7)
|
||||
haml (4.0.5)
|
||||
tilt
|
||||
hike (1.2.3)
|
||||
|
@ -284,7 +283,6 @@ DEPENDENCIES
|
|||
factory_girl_rails (~> 4.4.1)
|
||||
faraday (~> 0.9.0)
|
||||
font-awesome-sass (~> 4.1.0.0)
|
||||
gruff (~> 0.3.6)
|
||||
haml (~> 4.0.5)
|
||||
jquery-rails (~> 2.0.2)
|
||||
mysql2 (~> 0.3.15)
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
class ServersController < ApplicationController
|
||||
before_filter :get_server, except: [:index, :refresh, :new, :create]
|
||||
|
||||
def refresh
|
||||
Server.refresh
|
||||
render :text => t(:servers_updated)
|
||||
end
|
||||
|
||||
def index
|
||||
@servers = Server.hlds.active.ordered.all :include => :user
|
||||
@ns2 = Server.ns2.active.ordered.all :include => :user
|
||||
@hltvs = Server.hltvs.active.ordered.all :include => :user
|
||||
@officials = Server.ns2.active.ordered.where ["name LIKE ?", "%NSL%"]
|
||||
end
|
||||
|
||||
|
@ -25,15 +19,6 @@ class ServersController < ApplicationController
|
|||
raise AccessError unless @server.can_update? cuser
|
||||
end
|
||||
|
||||
def admin
|
||||
@result = @server.execute params[:query] if params[:query]
|
||||
raise AccessError unless @server.can_update? cuser
|
||||
|
||||
if request.xhr?
|
||||
render partial: 'response', layout: false
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@server = Server.new params[:server]
|
||||
@server.user = cuser
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
# title :string(255)
|
||||
# status :integer not null
|
||||
# category_id :integer
|
||||
# text :text
|
||||
# text :text(16777215)
|
||||
# user_id :integer
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# version :integer
|
||||
# text_parsed :text
|
||||
# text_parsed :text(16777215)
|
||||
# text_coding :integer default(0), not null
|
||||
#
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# votes :integer default(0), not null
|
||||
# status :integer default(0), not null
|
||||
#
|
||||
|
||||
class Gatherer < ActiveRecord::Base
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
# points1 :integer
|
||||
# points2 :integer
|
||||
# hltv_id :integer
|
||||
# caster_id :string(255)
|
||||
#
|
||||
|
||||
class Match < ActiveRecord::Base
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
# steam_profile :string(255)
|
||||
# achievements_parsed :string(255)
|
||||
# signature_parsed :string(255)
|
||||
# stream :string(255)
|
||||
# layout :string(255)
|
||||
#
|
||||
|
||||
class Profile < ActiveRecord::Base
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
# category_id :integer
|
||||
#
|
||||
|
||||
require "rcon"
|
||||
require "yaml"
|
||||
|
||||
class Server < ActiveRecord::Base
|
||||
|
@ -40,20 +39,16 @@ class Server < ActiveRecord::Base
|
|||
DOMAIN_HLDS = 0
|
||||
DOMAIN_HLTV = 1
|
||||
DOMAIN_NS2 = 2
|
||||
HLTV_IDLE = 1200
|
||||
DEMOS = "/var/www/virtual/ensl.org/hlds_l/ns/demos"
|
||||
QSTAT = "/usr/bin/quakestat"
|
||||
TMPFILE = "tmp/server.txt"
|
||||
|
||||
attr_accessor :rcon_handle, :pwd
|
||||
attr_accessor :pwd
|
||||
attr_protected :id, :user_id, :updated_at, :created_at, :map, :players, :maxplayers, :ping, :version
|
||||
|
||||
validates_length_of [:name, :dns,], :in => 1..30
|
||||
validates_length_of [:rcon, :password, :irc], :maximum => 30, :allow_blank => true
|
||||
validates_length_of :description, :maximum => 255, :allow_blank => true
|
||||
validates_format_of :ip, :with => /\A[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\z/
|
||||
validates_format_of :port, :with => /\A[0-9]{1,5}\z/
|
||||
validates_format_of :reservation, :with => /\A[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]{1,5}\z/, :allow_nil => true
|
||||
validates_format_of :port, :with => /\A[0-9]{1,5}\z/
|
||||
validates_format_of :reservation, :with => /\A[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]{1,5}\z/, :allow_nil => true
|
||||
validates_format_of :pwd, :with => /\A[A-Za-z0-9_\-]*\z/, :allow_nil => true
|
||||
|
||||
scope :ordered, :order => "name"
|
||||
|
@ -71,301 +66,79 @@ class Server < ActiveRecord::Base
|
|||
AND match_time > '#{(time.ago(Match::MATCH_LENGTH).utc).strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
AND match_time < '#{(time.ago(-Match::MATCH_LENGTH).utc).strftime("%Y-%m-%d %H:%M:%S")}'",
|
||||
:conditions => "matches.hltv_id IS NULL"} }
|
||||
scope :of_addr,
|
||||
lambda { |addr| {
|
||||
:conditions => {
|
||||
:ip => addr.match(/([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/)[0],
|
||||
:port => addr.match(/:([0-9]{1,5})/)[1] } } }
|
||||
scope :of_category,
|
||||
lambda { |category| {
|
||||
:conditions => {:category_id => category.id} }}
|
||||
|
||||
has_many :logs
|
||||
has_many :matches
|
||||
has_many :challenges
|
||||
belongs_to :user
|
||||
belongs_to :recordable, :polymorphic => true
|
||||
has_many :logs
|
||||
has_many :matches
|
||||
has_many :challenges
|
||||
belongs_to :user
|
||||
belongs_to :recordable, :polymorphic => true
|
||||
|
||||
before_create :set_category
|
||||
before_create :set_category
|
||||
|
||||
acts_as_versioned
|
||||
non_versioned_columns << 'name'
|
||||
non_versioned_columns << 'description'
|
||||
non_versioned_columns << 'dns'
|
||||
non_versioned_columns << 'ip'
|
||||
non_versioned_columns << 'port'
|
||||
non_versioned_columns << 'rcon'
|
||||
non_versioned_columns << 'password'
|
||||
non_versioned_columns << 'irc'
|
||||
non_versioned_columns << 'user_id'
|
||||
non_versioned_columns << 'official'
|
||||
non_versioned_columns << 'domain'
|
||||
non_versioned_columns << 'reservation'
|
||||
non_versioned_columns << 'recording'
|
||||
non_versioned_columns << 'idle'
|
||||
non_versioned_columns << 'default_id'
|
||||
non_versioned_columns << 'active'
|
||||
non_versioned_columns << 'recordable_type'
|
||||
non_versioned_columns << 'recordable_id'
|
||||
acts_as_versioned
|
||||
non_versioned_columns << 'name'
|
||||
non_versioned_columns << 'description'
|
||||
non_versioned_columns << 'dns'
|
||||
non_versioned_columns << 'ip'
|
||||
non_versioned_columns << 'port'
|
||||
non_versioned_columns << 'rcon'
|
||||
non_versioned_columns << 'password'
|
||||
non_versioned_columns << 'irc'
|
||||
non_versioned_columns << 'user_id'
|
||||
non_versioned_columns << 'official'
|
||||
non_versioned_columns << 'domain'
|
||||
non_versioned_columns << 'reservation'
|
||||
non_versioned_columns << 'recording'
|
||||
non_versioned_columns << 'idle'
|
||||
non_versioned_columns << 'default_id'
|
||||
non_versioned_columns << 'active'
|
||||
non_versioned_columns << 'recordable_type'
|
||||
non_versioned_columns << 'recordable_id'
|
||||
|
||||
def domains
|
||||
{DOMAIN_HLTV => "HLTV", DOMAIN_HLDS => "NS Server", DOMAIN_NS2 => "NS2 Server"}
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def addr
|
||||
ip + ":" + port.to_s
|
||||
end
|
||||
|
||||
def players_s
|
||||
if players.nil? or max_players.nil?
|
||||
"N/A"
|
||||
else
|
||||
players.to_s + " / " + max_players.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def recording_s
|
||||
return nil if self.domain != DOMAIN_HLTV
|
||||
# recording.to_i > 0 ? Match.find(recording).to_s : recording
|
||||
recording
|
||||
end
|
||||
|
||||
def reservation_s
|
||||
return nil if domain != DOMAIN_HLTV
|
||||
reservation
|
||||
end
|
||||
|
||||
def graphfile
|
||||
File.join("public", "images", "servers", id.to_s + ".png")
|
||||
end
|
||||
|
||||
def set_category
|
||||
self.category_id = (domain == DOMAIN_NS2 ? 45 : 44 )
|
||||
end
|
||||
|
||||
def after_validation_on_update
|
||||
if reservation_changed?
|
||||
rcon_connect
|
||||
if reservation == nil
|
||||
rcon_exec "stop"
|
||||
self.recording = nil
|
||||
self.recordable = nil
|
||||
self.idle = nil
|
||||
save_demos if self.recording
|
||||
hltv_stop
|
||||
else
|
||||
if changes['reservation'][0].nil?
|
||||
hltv_start
|
||||
rcon_exec "stop"
|
||||
self.recording = recordable.demo_name if recordable and recordable_type == "Match" or recordable_type == "Gather"
|
||||
rcon_exec "record demos/" + self.recording if self.recording
|
||||
end
|
||||
rcon_exec "serverpassword " + pwd
|
||||
rcon_exec "connect " + reservation
|
||||
self.idle = DateTime.now
|
||||
end
|
||||
rcon_disconnect
|
||||
end
|
||||
end
|
||||
|
||||
def execute command
|
||||
rcon_connect
|
||||
response = rcon_exec command
|
||||
rcon_disconnect
|
||||
response
|
||||
end
|
||||
|
||||
def rcon_connect
|
||||
self.rcon_handle = RCon::Query::Original.new(ip, port, rcon)
|
||||
end
|
||||
|
||||
def rcon_exec command
|
||||
response = rcon_handle.command(command)
|
||||
|
||||
Log.transaction do
|
||||
Log.add(self, Log::DOMAIN_RCON_COMMAND, command)
|
||||
if response.to_s.length > 0
|
||||
Log.add(self, Log::DOMAIN_RCON_RESPONSE, response)
|
||||
end
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
def rcon_disconnect
|
||||
rcon_handle.disconnect
|
||||
end
|
||||
|
||||
def hltv_start
|
||||
if nr = hltv_nr
|
||||
`screen -d -m -S "Hltv-#{nr[1]}" -c $HOME/.screenrc-hltv $HOME/hlds_l/hltv -ip 78.46.36.107 -port 28#{nr[1]}00 +exec ns/hltv#{nr[1]}.cfg`
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
|
||||
def hltv_nr
|
||||
self.name.match(/Tv \#([0-9])/)
|
||||
end
|
||||
|
||||
def hltv_stop
|
||||
if nr = hltv_nr
|
||||
sleep 5
|
||||
rcon_exec "exit"
|
||||
#`screen -S "Hltv-#{nr[1]}" -X 'quit'`
|
||||
end
|
||||
end
|
||||
|
||||
def save_demos
|
||||
dir = case recordable_type
|
||||
when "Match" then
|
||||
recordable.contest.demos
|
||||
when "Gather" then
|
||||
Directory.find(Directory::DEMOS_GATHERS)
|
||||
end
|
||||
|
||||
dir ||= Directory.find(Directory::DEMOS_DEFAULT)
|
||||
zip_path = File.join(dir.path, recording + ".zip")
|
||||
|
||||
Zip::ZipOutputStream::open(zip_path) do |zos|
|
||||
if recordable_type == "Match"
|
||||
zos.put_next_entry "readme.txt"
|
||||
zos.write "Team1: " + recordable.contester1.to_s + "\r\n"
|
||||
zos.write "Team2: " + recordable.contester2.to_s + "\r\n"
|
||||
zos.write "Date: " + recordable.match_time.to_s + "\r\n"
|
||||
zos.write "Contest: " + recordable.contest.to_s + "\r\n"
|
||||
zos.write "Server: " + recordable.server.addr + "\r\n" if recordable.server
|
||||
zos.write "HLTV: " + addr + "\r\n"
|
||||
zos.write YAML::dump(recordable.attributes).to_s
|
||||
end
|
||||
Dir.glob("#{DEMOS}/*").each do |file|
|
||||
if File.file?(file) and file.match(/#{recording}.*\.dem/)
|
||||
zos.put_next_entry File.basename(file)
|
||||
zos.write(IO.read(file))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DataFile.transaction do
|
||||
unless dbfile = DataFile.find_by_path(zip_path)
|
||||
dbfile = DataFile.new
|
||||
dbfile.path = zip_path
|
||||
dbfile.directory = dir
|
||||
dbfile.save!
|
||||
DataFile.update_all({:name => File.basename(zip_path)}, {:id => dbfile.id})
|
||||
end
|
||||
if recordable_type == "Match"
|
||||
recordable.demo = dbfile
|
||||
recordable.save
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def make_stats
|
||||
graph = Gruff::Line.new
|
||||
graph.title = name
|
||||
pings = []
|
||||
players = []
|
||||
labels = {}
|
||||
n = 0
|
||||
|
||||
for version in versions.all(:order => "updated_at DESC", :limit => 30).reverse
|
||||
pings << version.ping.to_i
|
||||
players << version.players.to_i
|
||||
labels[n] = version.updated_at.strftime("%H:%M") if n % 3 == 0
|
||||
n = n + 1
|
||||
end
|
||||
|
||||
graph.theme_37signals
|
||||
graph.data("Ping", pings, '#057fc0')
|
||||
graph.data("Players", players, '#ff0000')
|
||||
graph.labels = labels
|
||||
graph.write(graphfile)
|
||||
end
|
||||
|
||||
def default_record
|
||||
# if self.default
|
||||
# rcon_exec "record demos/auto-" + Verification.uncrap(default.name)
|
||||
# rcon_exec "serverpassword " + default.password
|
||||
# rcon_exec "connect " + default.addr
|
||||
# end
|
||||
end
|
||||
|
||||
def is_free time
|
||||
challenges.around(time).pending.count == 0 and matches.around(time).count == 0
|
||||
end
|
||||
|
||||
def can_create? cuser
|
||||
cuser
|
||||
end
|
||||
|
||||
def can_update? cuser
|
||||
cuser and cuser.admin? or user == cuser
|
||||
end
|
||||
|
||||
def can_destroy? cuser
|
||||
cuser and cuser.admin?
|
||||
end
|
||||
|
||||
def self.refresh
|
||||
servers = ""
|
||||
Server.hlds.active.all.each do |server|
|
||||
servers << " -a2s " + server.ip + ":" + server.port.to_s
|
||||
def domains
|
||||
{DOMAIN_HLTV => "HLTV", DOMAIN_HLDS => "NS Server", DOMAIN_NS2 => "NS2 Server"}
|
||||
end
|
||||
|
||||
file = File.join(Rails.root, TMPFILE)
|
||||
system "#{QSTAT} -xml #{servers} | grep -v '<name>' > #{file}"
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
doc = REXML::Document.new(File.new(file).read)
|
||||
doc.elements.each('qstat/server') do |server|
|
||||
hostname = server.elements['hostname'].text.split(':', 2)
|
||||
if s = Server.active.first(:conditions => {:ip => hostname[0], :port => hostname[1]})
|
||||
if server.elements.include? 'map'
|
||||
s.map = server.elements['map'].text
|
||||
s.players = server.elements['numplayers'].text.to_i
|
||||
s.max_players = server.elements['maxplayers'].text.to_i
|
||||
s.ping = server.elements['ping'].text
|
||||
s.map = server.elements['map'].text
|
||||
s.save
|
||||
s.make_stats
|
||||
end
|
||||
def addr
|
||||
ip + ":" + port.to_s
|
||||
end
|
||||
|
||||
def set_category
|
||||
self.category_id = (domain == DOMAIN_NS2 ? 45 : 44 )
|
||||
end
|
||||
|
||||
def is_free time
|
||||
challenges.around(time).pending.count == 0 and matches.around(time).count == 0
|
||||
end
|
||||
|
||||
def can_create? cuser
|
||||
cuser
|
||||
end
|
||||
|
||||
def can_update? cuser
|
||||
cuser and cuser.admin? or user == cuser
|
||||
end
|
||||
|
||||
def can_destroy? cuser
|
||||
cuser and cuser.admin?
|
||||
end
|
||||
|
||||
def self.move addr, newaddr, newpwd
|
||||
self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv|
|
||||
hltv.reservation = newaddr
|
||||
hltv.pwd = newpwd
|
||||
hltv.save!
|
||||
end
|
||||
end
|
||||
|
||||
servers = ""
|
||||
Server.hltvs.reserved.each do |server|
|
||||
servers << " -a2s #{server.reservation}"
|
||||
end
|
||||
|
||||
doc = REXML::Document.new(`#{QSTAT} -xml #{servers} | grep -v '<name>'`)
|
||||
doc.elements.each('qstat/server') do |server|
|
||||
hostname = server.elements['hostname'].text.split(':', 2)
|
||||
if s = Server.hltvs.reserved.first(:conditions => {:ip => hostname[0], :port => hostname[1]})
|
||||
if server.elements['numplayers'].text.to_i > 0
|
||||
s.update_attribute :idle, DateTime.now
|
||||
elsif (s.idle + HLTV_IDLE).past?
|
||||
s.reservation = nil
|
||||
s.save
|
||||
end
|
||||
def self.stop addr
|
||||
self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv|
|
||||
hltv.reservation = nil
|
||||
hltv.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.move addr, newaddr, newpwd
|
||||
self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv|
|
||||
hltv.reservation = newaddr
|
||||
hltv.pwd = newpwd
|
||||
hltv.save!
|
||||
end
|
||||
end
|
||||
|
||||
def self.stop addr
|
||||
self.hltvs.all(:conditions => {:reservation => addr}).each do |hltv|
|
||||
hltv.reservation = nil
|
||||
hltv.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<% @server.logs.recent.reverse_each do |log| %>
|
||||
<% if log.domain == Log::DOMAIN_RCON_COMMAND %>
|
||||
<pre class="command"><%= log.text %></pre>
|
||||
<% elsif log.domain == Log::DOMAIN_RCON_RESPONSE %>
|
||||
<pre class="response"><%= log.text %></pre>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,7 +0,0 @@
|
|||
<% @server.logs.recent.reverse_each do |log| %>
|
||||
<% if log.domain == Log::DOMAIN_RCON_COMMAND %>
|
||||
<pre class="command"><%= log.text %></pre>
|
||||
<% elsif log.domain == Log::DOMAIN_RCON_RESPONSE %>
|
||||
<pre class="response"><%= log.text %></pre>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,19 +0,0 @@
|
|||
<h1>
|
||||
RCON: <%= h @server %>
|
||||
</h1>
|
||||
|
||||
<%= image_tag "icons/spinner.gif",
|
||||
:align => "absmiddle",
|
||||
:id => "spinner",
|
||||
:style =>"display: none;" %>
|
||||
|
||||
<div class="wide box" id="serverLog">
|
||||
<%= render :partial => "response" %>
|
||||
</div>
|
||||
|
||||
<% form_tag remote: true, update: 'serverLog',
|
||||
:before => "Element.show('spinner')",
|
||||
:success => "Element.hide('spinner'); $('serverLog').scrollTop = $('serverLog').scrollHeight;" do %>
|
||||
<%= label_tag :query, "Rcon Command:" %>
|
||||
<%= text_field_tag "query", params['query'], :size => 30 %>
|
||||
<% end %>
|
|
@ -1,21 +0,0 @@
|
|||
<h1>
|
||||
Server Log: <%= h @server %>
|
||||
</h1>
|
||||
|
||||
<div class="box wide">
|
||||
<table class="data">
|
||||
<tr>
|
||||
<th width="10%">Date</th>
|
||||
<th width="10%">Type</th>
|
||||
<th width="80%">Message</th>
|
||||
</tr>
|
||||
|
||||
<% @server.logs.each do |log| %>
|
||||
<tr class="<%= cycle('even', 'odd') %>">
|
||||
<td><%= shorttime log.created_at %></td>
|
||||
<td><%= log.domains[log.domain] %></td>
|
||||
<td><%= h log.text %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
12
spec/factories/group.rb
Normal file
12
spec/factories/group.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
FactoryGirl.define do
|
||||
factory :group do
|
||||
sequence(:id) { |n| n + 100 } # Preserve first 100
|
||||
sequence(:name) { |n| "Group#{n}" }
|
||||
association :founder, factory: :user
|
||||
end
|
||||
|
||||
trait :admin do
|
||||
name "Admins"
|
||||
id Group::ADMINS
|
||||
end
|
||||
end
|
5
spec/factories/grouper.rb
Normal file
5
spec/factories/grouper.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
FactoryGirl.define do
|
||||
factory :grouper do
|
||||
sequence(:task) { |n| "Task#{n}" }
|
||||
end
|
||||
end
|
8
spec/factories/server.rb
Normal file
8
spec/factories/server.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
FactoryGirl.define do
|
||||
factory :server do
|
||||
sequence(:name) { |n| "ServerName#{n}" }
|
||||
sequence(:dns) { |n| "DNS#{n}" }
|
||||
sequence(:ip) { |n| "192.168.#{n % 255}.#{n}" }
|
||||
sequence(:port) { |n| "#{1000 + n}" }
|
||||
end
|
||||
end
|
|
@ -13,6 +13,13 @@ FactoryGirl.define do
|
|||
create(:profile, user: user)
|
||||
end
|
||||
|
||||
trait :admin do
|
||||
after(:create) do |user|
|
||||
group = create(:group, :admin)
|
||||
create :grouper, user: user, group: group
|
||||
end
|
||||
end
|
||||
|
||||
factory :user_with_team do
|
||||
after(:create) do |user|
|
||||
create(:team, founder: user)
|
||||
|
|
30
spec/features/servers/server_administration.rb
Normal file
30
spec/features/servers/server_administration.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Server Administration' do
|
||||
let!(:admin) { create :user, :admin }
|
||||
|
||||
background do
|
||||
sign_in_as admin
|
||||
end
|
||||
|
||||
scenario 'creating a server' do
|
||||
visit servers_path
|
||||
expect(page).to have_content('Listing Servers')
|
||||
click_link 'New server'
|
||||
test_server_creation_and_editing
|
||||
visit servers_path
|
||||
expect(page).to have_content Server.last.name
|
||||
end
|
||||
|
||||
feature 'Server deletion' do
|
||||
let!(:server) { create :server }
|
||||
scenario 'deleting a server' do
|
||||
visit servers_path
|
||||
expect(page).to have_content(server.name)
|
||||
visit server_path(server)
|
||||
click_link 'Delete Server'
|
||||
visit servers_path
|
||||
expect(page).to_not have_content(server.name)
|
||||
end
|
||||
end
|
||||
end
|
14
spec/features/servers/user_servers.rb
Normal file
14
spec/features/servers/user_servers.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'User created servers' do
|
||||
let!(:user) { create :user }
|
||||
|
||||
background do
|
||||
sign_in_as user
|
||||
end
|
||||
|
||||
scenario 'Creating and updating a server' do
|
||||
visit new_server_path
|
||||
test_server_creation_and_editing
|
||||
end
|
||||
end
|
98
spec/models/server_spec.rb
Normal file
98
spec/models/server_spec.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
# == Schema Information
|
||||
#
|
||||
# Table name: servers
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string(255)
|
||||
# description :string(255)
|
||||
# dns :string(255)
|
||||
# ip :string(255)
|
||||
# port :string(255)
|
||||
# rcon :string(255)
|
||||
# password :string(255)
|
||||
# irc :string(255)
|
||||
# user_id :integer
|
||||
# official :boolean
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# map :string(255)
|
||||
# players :integer
|
||||
# max_players :integer
|
||||
# ping :string(255)
|
||||
# version :integer
|
||||
# domain :integer default(0), not null
|
||||
# reservation :string(255)
|
||||
# recording :string(255)
|
||||
# idle :datetime
|
||||
# default_id :integer
|
||||
# active :boolean default(TRUE), not null
|
||||
# recordable_type :string(255)
|
||||
# recordable_id :integer
|
||||
# category_id :integer
|
||||
#
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Server do
|
||||
describe 'create' do
|
||||
it 'sets category to 45 if domain is NS2' do
|
||||
server = create :server, domain: Server::DOMAIN_NS2
|
||||
expect(server.category_id).to eq(45)
|
||||
end
|
||||
it 'sets category to 44 if domain is not NS2' do
|
||||
server = create :server, domain: Server::DOMAIN_HLDS
|
||||
expect(server.category_id).to eq(44)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'addr' do
|
||||
it 'returns properly formatted IP and port number' do
|
||||
ip = '1.1.1.1'
|
||||
port = '8000'
|
||||
server = create :server, ip: ip, port: port
|
||||
expect(server.addr).to eq('1.1.1.1:8000')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'to_s' do
|
||||
it 'returns server name' do
|
||||
server_name = "Foo"
|
||||
server = create :server, name: server_name
|
||||
expect(server.to_s).to eq(server_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Permissions' do
|
||||
let!(:user) { create :user }
|
||||
let!(:admin) { create :user, :admin }
|
||||
let!(:server_user) {create :user }
|
||||
let!(:server) { create :server, user: server_user }
|
||||
|
||||
describe 'can_create?' do
|
||||
it 'returns true for non-admins' do
|
||||
expect(server.can_create? user).to be_true
|
||||
end
|
||||
end
|
||||
|
||||
describe 'can_destroy?' do
|
||||
it 'returns true for admin' do
|
||||
expect(server.can_destroy? admin).to be_true
|
||||
end
|
||||
it 'returns false for non-admins' do
|
||||
expect(server.can_destroy? user).to be_false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'can_update?' do
|
||||
it 'returns true for admin' do
|
||||
expect(server.can_update? admin).to be_true
|
||||
end
|
||||
it 'returns true if server belongs to user' do
|
||||
expect(server.can_update? server_user).to be_true
|
||||
end
|
||||
it 'returns false for non-admins' do
|
||||
expect(server.can_update? user).to be_false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,6 +26,7 @@ RSpec.configure do |config|
|
|||
config.include FactoryGirl::Syntax::Methods
|
||||
config.include Controllers::JsonHelpers, type: :controller
|
||||
config.include Features::FormHelpers, type: :feature
|
||||
config.include Features::ServerHelpers, type: :feature
|
||||
config.include Features::SessionHelpers, type: :feature
|
||||
|
||||
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
||||
|
|
49
spec/support/features/server_helpers.rb
Normal file
49
spec/support/features/server_helpers.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
module Features
|
||||
module ServerHelpers
|
||||
def test_server_creation_and_editing
|
||||
dns = 'ServerDns.com'
|
||||
ip = '192.168.1.1'
|
||||
port = '8000'
|
||||
rcon = 'whatsrcon'
|
||||
password = 'secret'
|
||||
name = 'MyNsServer'
|
||||
description = 'My NS Server'
|
||||
irc = '#some_channel'
|
||||
|
||||
visit new_server_path
|
||||
fill_in 'Dns', with: dns
|
||||
fill_in 'server_ip', with: ip
|
||||
fill_in 'server_port', with: port
|
||||
fill_in 'Password', with: password
|
||||
fill_in 'Name', with: name
|
||||
fill_in 'Description', with: description
|
||||
fill_in 'Irc', with: irc
|
||||
check 'Available for officials?'
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_content(dns)
|
||||
expect(page).to have_content("#{ip}:#{port}")
|
||||
expect(page).to have_content(password)
|
||||
expect(page).to have_content(irc)
|
||||
expect(page).to have_content(description)
|
||||
|
||||
click_link 'Edit Server'
|
||||
|
||||
fill_in 'Dns', with: "#{dns}2"
|
||||
fill_in 'server_ip', with: "192.168.1.2"
|
||||
fill_in 'server_port', with: "8001"
|
||||
fill_in 'Password', with: "#{password}2"
|
||||
fill_in 'Name', with: "#{name}2"
|
||||
fill_in 'Description', with: "#{description}2"
|
||||
fill_in 'Irc', with: "#{irc}2"
|
||||
check 'Available for officials?'
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_content("192.168.1.2:8001")
|
||||
expect(page).to have_content("#{dns}2")
|
||||
expect(page).to have_content("#{password}2")
|
||||
expect(page).to have_content("#{irc}2")
|
||||
expect(page).to have_content("#{description}2")
|
||||
end
|
||||
end
|
||||
end
|
296
vendor/plugins/rcon/bin/rcontool
vendored
296
vendor/plugins/rcon/bin/rcontool
vendored
|
@ -1,296 +0,0 @@
|
|||
#!/usr/bin/env ruby
|
||||
################################################################
|
||||
#
|
||||
# rcontool - shell interface to rcon commands
|
||||
#
|
||||
# (C) 2006 Erik Hollensbe, License details below
|
||||
#
|
||||
# Use 'rcontool -h' for usage instructions.
|
||||
#
|
||||
# The compilation of software known as rcontool is distributed under the
|
||||
# following terms:
|
||||
# Copyright (C) 2005-2006 Erik Hollensbe. All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source form, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
# SUCH DAMAGE.
|
||||
#
|
||||
#
|
||||
################################################################
|
||||
|
||||
#
|
||||
# rubygems hack
|
||||
#
|
||||
|
||||
begin
|
||||
require 'rubygems'
|
||||
rescue LoadError => e
|
||||
end
|
||||
begin
|
||||
require 'rcon'
|
||||
require 'ip'
|
||||
rescue LoadError => e
|
||||
$stderr.puts "rcontool requires the rcon and ip libraries be installed."
|
||||
$stderr.puts "You can find them both via rubygems or at http://rubyforge.org."
|
||||
exit -1
|
||||
end
|
||||
|
||||
RCONTOOL_VERSION = '0.1.0'
|
||||
|
||||
require 'optparse'
|
||||
require 'ostruct'
|
||||
|
||||
#
|
||||
# Manages our options
|
||||
#
|
||||
|
||||
def get_options
|
||||
options = OpenStruct.new
|
||||
# ip address (IP::Address object)
|
||||
options.ip_address = nil
|
||||
# port (integer)
|
||||
options.port = nil
|
||||
# password
|
||||
options.password = nil
|
||||
# protocol type (one of :hlds, :source, :oldquake, :newquake)
|
||||
options.protocol_type = nil
|
||||
# verbose, spit out extra information
|
||||
options.verbose = false
|
||||
# command to execute on the server
|
||||
options.command = nil
|
||||
|
||||
optparse = OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{File.basename $0} <ip_address:port> <command> [options]"
|
||||
opts.separator ""
|
||||
opts.separator "Options:"
|
||||
|
||||
opts.on("--ip-address [ADDRESS]",
|
||||
"Provide an IP address to connect to. Does not take a port.") do |ip_address|
|
||||
if ! options.ip_address.nil?
|
||||
$stderr.puts "Error: you have already provided an IP Address."
|
||||
$stderr.puts opts
|
||||
exit -1
|
||||
end
|
||||
|
||||
options.ip_address = IP::Address.new(ip_address)
|
||||
end
|
||||
|
||||
opts.on("-r", "--port [PORT]",
|
||||
"Port to connect to.") do |port|
|
||||
if ! options.port.nil?
|
||||
$stderr.puts "Error: you have already provided a port."
|
||||
$stderr.puts opts
|
||||
exit -1
|
||||
end
|
||||
|
||||
options.port = port.to_i
|
||||
end
|
||||
|
||||
opts.on("-c", "--command [COMMAND]",
|
||||
"Command to run on the server.") do |command|
|
||||
if ! options.command.nil?
|
||||
$stderr.puts "Error: you have already provided a command."
|
||||
$stderr.puts opts
|
||||
exit -1
|
||||
end
|
||||
|
||||
options.command = command
|
||||
end
|
||||
|
||||
opts.on("-p", "--password [PASSWORD]",
|
||||
"Provide a password on the command line.") do |password|
|
||||
options.password = password
|
||||
end
|
||||
|
||||
opts.on("-f", "--password-from [FILENAME]",
|
||||
"Get the password from a file (use '/dev/fd/0' or '/dev/stdin' to read from Standard Input).") do |filename|
|
||||
if !filename.nil?
|
||||
f = File.open(filename)
|
||||
options.password = f.gets.chomp
|
||||
f.close
|
||||
else
|
||||
$stderr.puts "Error: filename (from -f) is not valid."
|
||||
$stderr.puts opts
|
||||
exit -1
|
||||
end
|
||||
end
|
||||
|
||||
opts.on("-t", "--protocol-type [TYPE]", [:hlds, :source, :oldquake, :newquake],
|
||||
"Type of rcon connection to make: (hlds, source, oldquake, newquake).",
|
||||
" Note: oldquake is quake1/quakeworld, newquake is quake2/3.") do |protocol_type|
|
||||
options.protocol_type = protocol_type
|
||||
end
|
||||
|
||||
opts.on("-v", "--[no-]verbose",
|
||||
"Run verbosely, print information about each packet recieved and turnaround times.") do |verbose|
|
||||
options.verbose = verbose
|
||||
end
|
||||
|
||||
opts.on("-h", "--help",
|
||||
"This help message.") do
|
||||
$stderr.puts opts
|
||||
exit -1
|
||||
end
|
||||
|
||||
opts.on("--version", "Print the version information.") do
|
||||
$stderr.puts "This is rcontool version #{RCONTOOL_VERSION},"
|
||||
$stderr.puts "it is located at #{File.expand_path $0}."
|
||||
exit -1
|
||||
end
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Note: IP, port, protocol type, password and command are required to function."
|
||||
opts.separator ""
|
||||
opts.separator "Examples (all are equivalent):"
|
||||
opts.separator "\t#{File.basename($0)} 10.0.0.11 status -t hlds -r 27015 -p foobar"
|
||||
opts.separator "\techo 'foobar' | #{File.basename($0)} 10.0.0.11:27015 status -t hlds -f /dev/stdin"
|
||||
opts.separator "\t#{File.basename($0)} --ip-address 10.0.0.11 --port 27015 -c status -t hlds -f file_with_password"
|
||||
opts.separator ""
|
||||
|
||||
end
|
||||
|
||||
################################################################
|
||||
#
|
||||
# This hackery is to help facilitate the bareword options if
|
||||
# they exist, while still allowing for the option parser
|
||||
# to work properly.
|
||||
#
|
||||
################################################################
|
||||
|
||||
s1 = ARGV.shift
|
||||
s2 = ARGV.shift
|
||||
|
||||
begin
|
||||
options.ip_address = IP::Address::IPv4.new(s1)
|
||||
options.command = s2
|
||||
rescue IP::AddressException => e
|
||||
# attempt to split it first... not sure how to best handle this situation
|
||||
begin
|
||||
ip,port = s1.split(/:/, 2)
|
||||
options.ip_address = IP::Address::IPv4.new(ip)
|
||||
options.port = port.to_i
|
||||
options.command = s2
|
||||
rescue Exception => e
|
||||
end
|
||||
|
||||
if [options.ip_address, options.port].include? nil
|
||||
ARGV.unshift(s2)
|
||||
ARGV.unshift(s1)
|
||||
end
|
||||
end
|
||||
|
||||
optparse.parse!
|
||||
|
||||
if [options.ip_address, options.protocol_type, options.port, options.password, options.command].include? nil
|
||||
$stderr.puts optparse
|
||||
exit -1
|
||||
end
|
||||
|
||||
return options
|
||||
end
|
||||
|
||||
def verbose(string)
|
||||
$stderr.puts string if $options.verbose
|
||||
end
|
||||
|
||||
def dump_source_packet(packet)
|
||||
if $options.verbose
|
||||
verbose "Request ID: #{packet.request_id}"
|
||||
verbose "Packet Size: #{packet.packet_size}"
|
||||
verbose "Response Type: #{packet.command_type}"
|
||||
end
|
||||
end
|
||||
|
||||
################################################################
|
||||
#
|
||||
# start main block
|
||||
#
|
||||
################################################################
|
||||
|
||||
$options = get_options
|
||||
|
||||
################################################################
|
||||
#
|
||||
# Source query
|
||||
#
|
||||
################################################################
|
||||
|
||||
if $options.protocol_type == :source
|
||||
verbose "Protocol type 'SOURCE' selected."
|
||||
|
||||
rcon = RCon::Query::Source.new($options.ip_address.ip_address, $options.port)
|
||||
|
||||
# if we have a verbose request, give all the information we can about
|
||||
# the query, including the packet information.
|
||||
rcon.return_packets = $options.verbose
|
||||
|
||||
verbose "Attempting authentication to #{$options.ip_address.ip_address}:#{$options.port} with password '#{$options.password}'"
|
||||
|
||||
value = rcon.auth $options.password
|
||||
|
||||
dump_source_packet value
|
||||
|
||||
if ($options.verbose && value.command_type == RCon::Packet::Source::RESPONSE_AUTH) || value
|
||||
verbose "Authentication succeeded. Sending command: '#{$options.command}'"
|
||||
|
||||
value = rcon.command $options.command
|
||||
|
||||
dump_source_packet value
|
||||
verbose ""
|
||||
|
||||
if $options.verbose
|
||||
puts value.string1
|
||||
else
|
||||
puts value
|
||||
end
|
||||
|
||||
exit 0
|
||||
else
|
||||
$stderr.puts "Authentication failed."
|
||||
exit 1
|
||||
end
|
||||
|
||||
################################################################
|
||||
#
|
||||
# Original Query
|
||||
#
|
||||
################################################################
|
||||
|
||||
else
|
||||
rcon = nil
|
||||
case $options.protocol_type
|
||||
when :hlds
|
||||
verbose "Protocol type 'HLDS' selected"
|
||||
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
|
||||
RCon::Query::Original::HLDS)
|
||||
when :oldquake
|
||||
verbose "Protocol type 'OLDQUAKE' selected"
|
||||
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
|
||||
RCon::Query::Original::QUAKEWORLD)
|
||||
when :newquake
|
||||
verbose "Protocol type 'NEWQUAKE' selected"
|
||||
rcon = RCon::Query::Original.new($options.ip_address.ip_address, $options.port, $options.password,
|
||||
RCon::Query::Original::NEWQUAKE)
|
||||
end
|
||||
verbose "Attempting transmission to #{$options.ip_address.ip_address}:#{$options.port}"
|
||||
verbose "Using password: '#{$options.password}' and sending command: '#{$options.command}'"
|
||||
verbose ""
|
||||
string = rcon.command($options.command)
|
||||
|
||||
puts string
|
||||
exit 0
|
||||
end
|
||||
|
499
vendor/plugins/rcon/lib/rcon.rb
vendored
499
vendor/plugins/rcon/lib/rcon.rb
vendored
|
@ -1,499 +0,0 @@
|
|||
# encoding: US-ASCII
|
||||
|
||||
require 'socket'
|
||||
|
||||
#
|
||||
# RCon is a module to work with Quake 1/2/3, Half-Life, and Half-Life
|
||||
# 2 (Source Engine) RCon (Remote Console) protocols.
|
||||
#
|
||||
# Version:: 0.2.0
|
||||
# Author:: Erik Hollensbe <erik@hollensbe.org>
|
||||
# License:: BSD
|
||||
# Contact:: erik@hollensbe.org
|
||||
# Copyright:: Copyright (c) 2005-2006 Erik Hollensbe
|
||||
#
|
||||
# The relevant modules to query RCon are in the RCon::Query namespace,
|
||||
# under RCon::Query::Original (for Quake 1/2/3 and Half-Life), and
|
||||
# RCon::Query::Source (for HL2 and CS: Source, and other Source Engine
|
||||
# games). The RCon::Packet namespace is used to manage complex packet
|
||||
# structures if required. The Original protocol does not require
|
||||
# this, but Source does.
|
||||
#
|
||||
# Usage is fairly simple:
|
||||
#
|
||||
# # Note: Other classes have different constructors
|
||||
#
|
||||
# rcon = RCon::Query::Source.new("10.0.0.1", 27015)
|
||||
#
|
||||
# rcon.auth("foobar") # source only
|
||||
#
|
||||
# rcon.command("mp_friendlyfire") => "mp_friendlyfire = 1"
|
||||
#
|
||||
# rcon.cvar("mp_friendlyfire") => 1
|
||||
#
|
||||
#--
|
||||
#
|
||||
# The compilation of software known as rcon.rb is distributed under the
|
||||
# following terms:
|
||||
# Copyright (C) 2005-2006 Erik Hollensbe. All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source form, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
# SUCH DAMAGE.
|
||||
#
|
||||
#++
|
||||
|
||||
|
||||
class RCon
|
||||
class Packet
|
||||
# placeholder so ruby doesn't bitch
|
||||
end
|
||||
class Query
|
||||
|
||||
#
|
||||
# Convenience method to scrape input from cvar output and return that data.
|
||||
# Returns integers as a numeric type if possible.
|
||||
#
|
||||
# ex: rcon.cvar("mp_friendlyfire") => 1
|
||||
#
|
||||
|
||||
def cvar(cvar_name)
|
||||
response = command(cvar_name)
|
||||
match = /^.+?\s(?:is|=)\s"([^"]+)".*$/.match response
|
||||
match = match[1]
|
||||
if /\D/.match match
|
||||
return match
|
||||
else
|
||||
return match.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# RCon::Packet::Source generates a packet structure useful for
|
||||
# RCon::Query::Source protocol queries.
|
||||
#
|
||||
# This class is primarily used internally, but is available if you
|
||||
# want to do something more advanced with the Source RCon
|
||||
# protocol.
|
||||
#
|
||||
# Use at your own risk.
|
||||
#
|
||||
|
||||
class RCon::Packet::Source
|
||||
# execution command
|
||||
COMMAND_EXEC = 2
|
||||
# auth command
|
||||
COMMAND_AUTH = 3
|
||||
# auth response
|
||||
RESPONSE_AUTH = 2
|
||||
# normal response
|
||||
RESPONSE_NORM = 0
|
||||
# packet trailer
|
||||
TRAILER = "\x00\x00"
|
||||
|
||||
# size of the packet (10 bytes for header + string1 length)
|
||||
attr_accessor :packet_size
|
||||
# Request Identifier, used in managing multiple requests at once
|
||||
attr_accessor :request_id
|
||||
# Type of command, normally COMMAND_AUTH or COMMAND_EXEC. In response packets, RESPONSE_AUTH or RESPONSE_NORM
|
||||
attr_accessor :command_type
|
||||
# First string, the only used one in the protocol, contains
|
||||
# commands and responses. Null terminated.
|
||||
attr_accessor :string1
|
||||
# Second string, unused by the protocol. Null terminated.
|
||||
attr_accessor :string2
|
||||
|
||||
#
|
||||
# Generate a command packet to be sent to an already
|
||||
# authenticated RCon connection. Takes the command as an
|
||||
# argument.
|
||||
#
|
||||
def command(string)
|
||||
@request_id = rand(1000)
|
||||
@string1 = string
|
||||
@string2 = TRAILER
|
||||
@command_type = COMMAND_EXEC
|
||||
|
||||
@packet_size = build_packet.length
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
#
|
||||
# Generate an authentication packet to be sent to a newly
|
||||
# started RCon connection. Takes the RCon password as an
|
||||
# argument.
|
||||
#
|
||||
def auth(string)
|
||||
@request_id = rand(1000)
|
||||
@string1 = string
|
||||
@string2 = TRAILER
|
||||
@command_type = COMMAND_AUTH
|
||||
|
||||
@packet_size = build_packet.length
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
#
|
||||
# Builds a packet ready to deliver, without the size prepended.
|
||||
# Used to calculate the packet size, use #to_s to get the packet
|
||||
# that srcds actually needs.
|
||||
#
|
||||
def build_packet
|
||||
return [@request_id, @command_type, @string1, @string2].pack("VVa#{@string1.length}a2")
|
||||
end
|
||||
|
||||
# Returns a string representation of the packet, useful for
|
||||
# sending and debugging. This include the packet size.
|
||||
def to_s
|
||||
packet = build_packet
|
||||
@packet_size = packet.length
|
||||
return [@packet_size].pack("V") + packet
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#
|
||||
# RCon::Query::Original queries Quake 1/2/3 and Half-Life servers
|
||||
# with the rcon protocol. This protocol travels over UDP to the
|
||||
# game server port, and requires an initial authentication step,
|
||||
# the information of which is provided at construction time.
|
||||
#
|
||||
# Some of the work here (namely the RCon packet structure) was taken
|
||||
# from the KKRcon code, which is written in perl.
|
||||
#
|
||||
# One query per authentication is allowed.
|
||||
#
|
||||
|
||||
class RCon::Query::Original < RCon::Query
|
||||
# HLDS-Based Servers
|
||||
HLDS = "l"
|
||||
# QuakeWorld/Quake 1 Servers
|
||||
QUAKEWORLD = "n"
|
||||
# Quake 2/3 Servers
|
||||
NEWQUAKE = ""
|
||||
|
||||
# Request to be sent to server
|
||||
attr_reader :request
|
||||
# Response from server
|
||||
attr_reader :response
|
||||
# Challenge ID (served by server-side of connection)
|
||||
attr_reader :challenge_id
|
||||
# UDPSocket object
|
||||
attr_reader :socket
|
||||
# Host of connection
|
||||
attr_reader :host
|
||||
# Port of connection
|
||||
attr_reader :port
|
||||
# RCon password
|
||||
attr_reader :password
|
||||
# type of server
|
||||
attr_reader :server_type
|
||||
|
||||
#
|
||||
# Creates a RCon::Query::Original object for use.
|
||||
#
|
||||
# The type (the default of which is HLDS), has multiple possible
|
||||
# values:
|
||||
#
|
||||
# HLDS - Half Life 1 (will not work with older versions of HLDS)
|
||||
#
|
||||
# QUAKEWORLD - QuakeWorld/Quake 1
|
||||
#
|
||||
# NEWQUAKE - Quake 2/3 (and many derivatives)
|
||||
#
|
||||
|
||||
def initialize(host, port, password, type=HLDS)
|
||||
@host = host
|
||||
@port = port
|
||||
@password = password
|
||||
@server_type = type
|
||||
end
|
||||
|
||||
#
|
||||
# Sends a request given as the argument, and returns the
|
||||
# response as a string.
|
||||
#
|
||||
def command(request)
|
||||
@request = request
|
||||
@challenge_id = nil
|
||||
|
||||
establish_connection
|
||||
|
||||
@socket.print "\xFF" * 4 + "challenge rcon\n\x00"
|
||||
|
||||
tmp = retrieve_socket_data
|
||||
challenge_id = /challenge rcon (\d+)/.match tmp
|
||||
if challenge_id
|
||||
@challenge_id = challenge_id[1]
|
||||
end
|
||||
|
||||
if @challenge_id.nil?
|
||||
raise RCon::NetworkException.new("RCon challenge ID never returned: wrong rcon password?")
|
||||
end
|
||||
|
||||
@socket.print "\xFF" * 4 + "rcon #{@challenge_id} \"#{@password}\" #{@request}\n\x00"
|
||||
@response = retrieve_socket_data
|
||||
|
||||
@response.sub!(/^\xFF\xFF\xFF\xFF#{@server_type}/, "")
|
||||
@response.sub!(/\x00+$/, "")
|
||||
|
||||
return @response
|
||||
end
|
||||
|
||||
#
|
||||
# Disconnects the RCon connection.
|
||||
#
|
||||
def disconnect
|
||||
if @socket
|
||||
@socket.close
|
||||
@socket = nil
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
#
|
||||
# Establishes the connection.
|
||||
#
|
||||
def establish_connection
|
||||
if @socket.nil?
|
||||
@socket = UDPSocket.new
|
||||
@socket.connect(@host, @port)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Generic method to pull data from the socket.
|
||||
#
|
||||
|
||||
def retrieve_socket_data
|
||||
return "" if @socket.nil?
|
||||
|
||||
retval = ""
|
||||
loop do
|
||||
break unless IO.select([@socket], nil, nil, 10)
|
||||
packet = @socket.recv(8192)
|
||||
retval << packet
|
||||
break if packet.length < 8192
|
||||
end
|
||||
|
||||
return retval
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#
|
||||
# RCon::Query::Source sends queries to a "Source" Engine server,
|
||||
# such as Half-Life 2: Deathmatch, Counter-Strike: Source, or Day
|
||||
# of Defeat: Source.
|
||||
#
|
||||
# Note that one authentication packet needs to be sent to send
|
||||
# multiple commands. Sending multiple authentication packets may
|
||||
# damage the current connection and require it to be reset.
|
||||
#
|
||||
# Note: If the attribute 'return_packets' is set to true, the full
|
||||
# RCon::Packet::Source object is returned, instead of just a string
|
||||
# with the headers stripped. Useful for debugging.
|
||||
#
|
||||
|
||||
class RCon::Query::Source < RCon::Query
|
||||
# RCon::Packet::Source object that was sent as a result of the last query
|
||||
attr_reader :packet
|
||||
# TCPSocket object
|
||||
attr_reader :socket
|
||||
# Host of connection
|
||||
attr_reader :host
|
||||
# Port of connection
|
||||
attr_reader :port
|
||||
# Authentication Status
|
||||
attr_reader :authed
|
||||
# return full packet, or just data?
|
||||
attr_accessor :return_packets
|
||||
|
||||
#
|
||||
# Given a host and a port (dotted-quad or hostname OK), creates
|
||||
# a RCon::Query::Source object. Note that this will still
|
||||
# require an authentication packet (see the auth() method)
|
||||
# before commands can be sent.
|
||||
#
|
||||
|
||||
def initialize(host, port)
|
||||
@host = host
|
||||
@port = port
|
||||
@socket = nil
|
||||
@packet = nil
|
||||
@authed = false
|
||||
@return_packets = false
|
||||
end
|
||||
|
||||
#
|
||||
# See RCon::Query#cvar.
|
||||
#
|
||||
|
||||
def cvar(cvar_name)
|
||||
return_packets = @return_packets
|
||||
@return_packets = false
|
||||
response = super
|
||||
@return_packets = return_packets
|
||||
return response
|
||||
end
|
||||
|
||||
#
|
||||
# Sends a RCon command to the server. May be used multiple times
|
||||
# after an authentication is successful.
|
||||
#
|
||||
# See the class-level documentation on the 'return_packet' attribute
|
||||
# for return values. The default is to return a string containing
|
||||
# the response.
|
||||
#
|
||||
|
||||
def command(command)
|
||||
|
||||
if ! @authed
|
||||
raise RCon::NetworkException.new("You must authenticate the connection successfully before sending commands.")
|
||||
end
|
||||
|
||||
@packet = RCon::Packet::Source.new
|
||||
@packet.command(command)
|
||||
|
||||
@socket.print @packet.to_s
|
||||
rpacket = build_response_packet
|
||||
|
||||
if rpacket.command_type != RCon::Packet::Source::RESPONSE_NORM
|
||||
raise RCon::NetworkException.new("error sending command: #{rpacket.command_type}")
|
||||
end
|
||||
|
||||
if @return_packets
|
||||
return rpacket
|
||||
else
|
||||
return rpacket.string1
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Requests authentication from the RCon server, given a
|
||||
# password. Is only expected to be used once.
|
||||
#
|
||||
# See the class-level documentation on the 'return_packet' attribute
|
||||
# for return values. The default is to return a true value if auth
|
||||
# succeeded.
|
||||
#
|
||||
|
||||
def auth(password)
|
||||
establish_connection
|
||||
|
||||
@packet = RCon::Packet::Source.new
|
||||
@packet.auth(password)
|
||||
|
||||
@socket.print @packet.to_s
|
||||
# on auth, one junk packet is sent
|
||||
rpacket = nil
|
||||
2.times { rpacket = build_response_packet }
|
||||
|
||||
if rpacket.command_type != RCon::Packet::Source::RESPONSE_AUTH
|
||||
raise RCon::NetworkException.new("error authenticating: #{rpacket.command_type}")
|
||||
end
|
||||
|
||||
@authed = true
|
||||
if @return_packets
|
||||
return rpacket
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :authenticate, :auth
|
||||
|
||||
#
|
||||
# Disconnects from the Source server.
|
||||
#
|
||||
|
||||
def disconnect
|
||||
if @socket
|
||||
@socket.close
|
||||
@socket = nil
|
||||
@authed = false
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
#
|
||||
# Builds a RCon::Packet::Source packet based on the response
|
||||
# given by the server.
|
||||
#
|
||||
def build_response_packet
|
||||
rpacket = RCon::Packet::Source.new
|
||||
total_size = 0
|
||||
request_id = 0
|
||||
type = 0
|
||||
response = ""
|
||||
message = ""
|
||||
|
||||
|
||||
loop do
|
||||
break unless IO.select([@socket], nil, nil, 10)
|
||||
|
||||
#
|
||||
# TODO: clean this up - read everything and then unpack.
|
||||
#
|
||||
|
||||
tmp = @socket.recv(14)
|
||||
if tmp.nil?
|
||||
return nil
|
||||
end
|
||||
size, request_id, type, message = tmp.unpack("VVVa*")
|
||||
total_size += size
|
||||
|
||||
# special case for authentication
|
||||
break if message.sub!(/\x00\x00$/, "")
|
||||
|
||||
response << message
|
||||
|
||||
# the 'size - 10' here accounts for the fact that we've snarfed 14 bytes,
|
||||
# the size (which is 4 bytes) is not counted, yet represents the rest
|
||||
# of the packet (which we have already taken 10 bytes from)
|
||||
|
||||
tmp = @socket.recv(size - 10)
|
||||
response << tmp
|
||||
response.sub!(/\x00\x00$/, "")
|
||||
end
|
||||
|
||||
rpacket.packet_size = total_size
|
||||
rpacket.request_id = request_id
|
||||
rpacket.command_type = type
|
||||
|
||||
# strip nulls (this is actually the end of string1 and string2)
|
||||
rpacket.string1 = response.sub(/\x00\x00$/, "")
|
||||
return rpacket
|
||||
end
|
||||
|
||||
# establishes a connection to the server.
|
||||
def establish_connection
|
||||
if @socket.nil?
|
||||
@socket = TCPSocket.new(@host, @port)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Exception class for network errors
|
||||
class RCon::NetworkException < Exception
|
||||
end
|
13
vendor/plugins/rcon/rcon.gemspec
vendored
13
vendor/plugins/rcon/rcon.gemspec
vendored
|
@ -1,13 +0,0 @@
|
|||
spec = Gem::Specification.new
|
||||
spec.name = "rcon"
|
||||
spec.version = "0.2.1"
|
||||
spec.author = "Erik Hollensbe"
|
||||
spec.email = "erik@hollensbe.org"
|
||||
spec.summary = "Ruby class to work with Quake 1/2/3, Half-Life and Source Engine rcon (remote console)"
|
||||
spec.has_rdoc = true
|
||||
spec.autorequire = "rcon"
|
||||
spec.bindir = 'bin'
|
||||
spec.executables << 'rcontool'
|
||||
spec.add_dependency('ip', '>= 0.2.1')
|
||||
spec.files = Dir['lib/rcon.rb'] + Dir['bin/rcontool']
|
||||
spec.rubyforge_project = 'rcon'
|
1360
vendor/plugins/rcon/setup.rb
vendored
1360
vendor/plugins/rcon/setup.rb
vendored
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue