Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
  • 5 commits
  • 9 files changed
  • 0 commit comments
  • 1 contributor
View
1 Gemfile
@@ -13,7 +13,6 @@ gem 'json'
gem 'omniauth'
gem 'omniauth-openid', :git => 'git://github.com/reu/omniauth-openid.git'
gem 'omniauth-steam'
-gem 'rcon'
# Gems used only for assets and not required
View
4 Gemfile.lock
@@ -53,7 +53,6 @@ GEM
hike (1.2.1)
hpricot (0.8.6)
i18n (0.6.0)
- ip (0.2.2)
journey (1.0.3)
jquery-rails (2.0.0)
railties (>= 3.2.0.beta, < 5.0)
@@ -99,8 +98,6 @@ GEM
rdoc (~> 3.4)
thor (~> 0.14.6)
rake (0.9.2.2)
- rcon (0.2.1)
- ip (>= 0.2.1)
rdoc (3.12)
json (~> 1.4)
ruby-openid (2.1.8)
@@ -138,7 +135,6 @@ DEPENDENCIES
omniauth-steam
pg
rails (= 3.2.1)
- rcon
sass-rails (~> 3.2.3)
sqlite3
uglifier (>= 1.0.3)
View
35 app/assets/stylesheets/application.css.scss
@@ -21,8 +21,8 @@ b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
-article, aside, canvas, details, embed,
-figure, figcaption, footer, header, hgroup,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
@@ -33,7 +33,7 @@ time, mark, audio, video {
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
-article, aside, details, figcaption, figure,
+article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
@@ -56,28 +56,31 @@ table {
border-spacing: 0;
}
-body {
+body {
/* background: image-url('background.png') repeat; */
background: image-url('bottom-bg.jpg') center bottom fixed no-repeat #000;
- margin: 0;
+ margin: 0;
font-family: Helvetica, Arial, Sans-Serif;
font-size: 14px;
- line-height: 18px;
+ line-height: 18px;
}
h1 { margin: 0px 0px 20px; font-size: 32px; }
small { font-size: 12px; }
-#container {
- width: 920px;
- margin: 10px auto 30px;
- background-color: rgba(255,255,255,0.8);
+strong { font-weight: bold; }
+#container {
+ width: 920px;
+ margin: 10px auto 30px;
+ background-color: rgba(255,255,255,0.8);
border-radius: 8px;
- border: 1px solid #000;
- height: 100%;
+ border: 1px solid #000;
+ height: 100%;
box-shadow: 3px 3px 10px #2F2F2F;
padding: 20px;
-
+
}
+.spacer{border:0;clear:both;float:none;height:0;margin:0;padding:0;width:0;}
+
table {
margin-bottom: 20px;
@@ -90,9 +93,9 @@ table {
#kick { font-size: 32px; font-weight: bold; }
-#user-nav {
- float: right;
+#user-nav {
+ float: right;
width: 200px;
- font-size: 12px;
+ font-size: 12px;
text-align: right;
}
View
49 app/models/group.rb
@@ -1,43 +1,56 @@
class Group
attr_accessor :name
-
+
def initialize(name)
require 'open-uri'
self.name = name
end
-
+
def member_ids
- doc = Hpricot::XML(raw_xml)
- doc.search(:steamID64).map(&:inner_html)
+ member_list.split("\n")
end
-
+
+ def cache_path
+ File.join(Rails.root, 'tmp', "group_cache_#{name}.txt")
+ end
+
private
- def cache_path
- File.join(Rails.root, 'tmp', "group_cache_#{name}.xml")
- end
-
- def raw_xml
+ def member_list
if !File.exist?(cache_path) || File.mtime(cache_path) < lambda { 1.hours.ago }.call
remote_xml
else
- local_xml
+ local_list
end
end
-
+
def remote_xml
require 'open-uri'
begin
+ current_page = 1
+ # Get the first page and write it
+ xml = URI.parse("http://steamcommunity.com/groups/#{name}/memberslistxml/?xml=1&p=#{current_page}").read
+ doc = Hpricot::XML(xml)
f = File.new(cache_path, "w")
- xml = URI.parse("http://steamcommunity.com/groups/#{name}/memberslistxml/?xml=1").read
- f.puts xml
+ f.puts doc.search(:steamID64).map(&:inner_html).join("\n")
f.close
- xml
+
+ # Get subsequent pages
+ while doc.search(:totalPages).inner_html.to_i > current_page
+ current_page += 1
+ xml = URI.parse("http://steamcommunity.com/groups/#{name}/memberslistxml/?xml=1&p=#{current_page}").read
+ doc = Hpricot::XML(xml)
+ f = File.new(cache_path, "a")
+ f.puts doc.search(:steamID64).map(&:inner_html).join("\n")
+ f.close
+ end
+
+ local_list
rescue OpenURI::HTTPError
- local_xml
+ local_list
end
end
-
- def local_xml
+
+ def local_list
begin
File.open(cache_path, "rb").read
rescue
View
41 app/models/player_list.rb
@@ -1,27 +1,27 @@
class PlayerList
def initialize
@players = []
+ @remote_attempts = 0
regex = /^#\s+\d+\s+\"(.+)\"\s+([_A-Za-z:0-9]+)\s/
- raise "Error getting status." if raw_status.blank?
raw_status.split("\n")[8..-1].each do |line|
match = line.match(regex)
- @players << Player.new(match.captures[0], match.captures[1]) if match
+ @players << Player.new(match.captures[0], match.captures[1]) if match && match.captures[0] != "replay"
end
end
-
-
+
+
def each(&blk)
@players.each(&blk)
end
-
+
def size
@players.size
end
-
+
def players
@players
end
-
+
private
def raw_status
if !File.exist?(cache_path) || File.mtime(cache_path) < lambda { 5.minutes.ago }.call
@@ -30,29 +30,32 @@ def raw_status
local_status
end
end
-
+
def remote_status
begin
+ @remote_attempts += 1
rcon = RconConnection.new
- f = File.new(cache_path, "w")
+ f = File.new(cache_path, "wb")
status = rcon.command('status')
f.puts status
f.close
status
- rescue RCon::NetworkException
- local_status
+ rescue RCon::NetworkException => e
+ puts "[Rcon Error] " + e
+ if @remote_attempts < 3
+ local_status
+ else
+ raise "Unable to retrieve remote status and local status cache empty."
+ end
end
end
-
+
def local_status
- begin
- File.open(cache_path, "rb").read
- rescue
- raise "Could not open status"
- end
+ status = File.open(cache_path, "rb").read
+ status.blank? ? remote_status : status
end
-
+
def cache_path
File.join(Rails.root, 'tmp', 'status_cache.txt')
end
-end
+end
View
2 app/models/rcon_connection.rb
@@ -1,3 +1,5 @@
+require 'rcon'
+
class RconConnection
def initialize
@rcon = RCon::Query::Source.new(ENV["RCON_ADDRESS"], ENV["RCON_PORT"] || 27015)
View
28 app/views/layouts/application.html.haml
@@ -12,7 +12,7 @@
= raw(flash[:notice])
- if flash[:warning] || flash[:alert]
#warning= raw(flash[:warning]) || raw(flash[:alert])
-
+
#container
#user-nav
- if current_user
@@ -25,5 +25,31 @@
- else
= link_to image_tag("sits_large_border.png", :alt => "Sign in with Steam"), "/auth/steam"
+ %p
+ %br/
+ %small
+ - if File.exist?(File.join(Rails.root, 'tmp', 'status_cache.txt'))
+ %strong Server Status Last Update:
+ %br/
+ = File.mtime(File.join(Rails.root, 'tmp', 'status_cache.txt')).strftime('%B %e, %Y at %l:%M%p')
+ %br/
+ - if File.exist?(File.join(Rails.root, 'tmp', 'group_cache_lostcontinents.xml'))
+ %strong Group Member List Last Update:
+ %br/
+ = File.mtime(File.join(Rails.root, 'tmp', 'group_cache_lostcontinents.xml')).strftime('%B %e, %Y at %l:%M%p')
+ %br/
+ %strong Current Server Time:
+ %br/
+ = Time.now.strftime('%l:%M%p on %B %e, %Y')
%h1 Pubby Kicker
= yield
+ %br/
+ %br/
+
+ .spacer
+
+ :erb
+ <a href="http://www.gametracker.com/server_info/68.232.183.80:27015/" target="_blank"><img src="http://cache.www.gametracker.com/server_info/68.232.183.80:27015/b_350_20_692108_381007_FFFFFF_000000.png" border="0" width="350" height="20" alt=""/></a>
+ <br />
+ <a href="http://www.gameservers.com/clanpay/?clanid=6d7a01683ab9e776a78a540fa2ceab52">Donations</a>
+
View
16 app/views/players/index.html.haml
@@ -6,16 +6,14 @@
%tr
%td= player.name
%td= player.pubby? ? "Yes" : "No"
+ - if @players.size == 0
+ %tr
+ %td{:colspan => 2} No players!
+
- if current_user && !current_user.pubby?
%tr
%td{:colspan => 2}= button_to "Kick a Pubby", "/players/kick", :class => "button", :disable_with => "Ruining someone's day...", :id => "kick"
-%p
- List updates every 3 minutes or on kick.
- %br/
- %small
- Last update:
- = File.mtime(File.join(Rails.root, 'tmp', 'status_cache.txt'))
- %br/
- Current Time:
- = Time.now
+%p
+ List updates every 3 minutes or on kick.
+ %br/
View
496 lib/rcon.rb
@@ -0,0 +1,496 @@
+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 && rpacket.string1.nil?
+ raise RCon::NetworkException.new("error sending command: #{rpacket.command_type} " + rpacket.to_yaml)
+ 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

No commit comments for this range

Something went wrong with that request. Please try again.