source: Serveurs/flashpolicyd/flashpolicyd-2.1/flashpolicyd.rb @ 2844

Last change on this file since 2844 was 2844, checked in by kent1, 10 years ago

Import des deux serveurs "Flash Policy Server"

File size: 12.0 KB
Line 
1#!/usr/bin/ruby
2# == Synopsis
3#
4# flashpolicyd: Serve Adobe Flash Policy XML files to clients
5#
6# == Description
7# Server to serve up flash policy xml files for flash clients since
8# player version 9,0,124,0. (Player 9 update 3)
9#
10# See http://www.adobe.com/devnet/flashplayer/articles/fplayer9_security_04.html
11# for more information, this needs to run as root since it should listen on
12# port 843 which on a unix machine needs root to listen on that socket.
13#
14# == Signals
15# * USR1 signal prints a single line stat message, during
16#   normal running this stat will be printed every 30 minutes by default, settable
17#   using --logfreq
18# * USR2 signal dumps the current threads and their statusses
19# * HUP signal will toggle debug mode which will print more lines in the log file
20# * TERM signal will exit the process closing all the sockets
21#
22# == Usage
23# flashpolicyd [OPTIONS]
24#
25# --help, -h:
26#   Show Help
27#
28# --verbose
29#   Turns on verbose logging to log file - can also be turned on and off at runtime using -HUP signals
30#
31# --xml
32#   XML File to Serve to clients, read at startup only
33#
34# --timeout, -t
35#   If a request does not complete within this time, close the socket,
36#   default is 10 seconds
37#
38# --logfreq, -l
39#   How often to log stats to log file, default 1800 seconds
40#
41# --logfile
42#   Where to write log lines too
43#
44# == Download and Further Information
45# Latest versions, installation documentation and other related info can be found
46# at http://www.devco.net/pubwiki/FlashPolicyd
47#
48# == Author
49# R.I.Pienaar <rip@devco.net>
50
51require "socket"
52require "logger"
53require "ostruct"
54require "thread"
55require "timeout"
56require 'getoptlong'
57
58opts = GetoptLong.new(
59    [ '--xml', GetoptLong::REQUIRED_ARGUMENT],
60    [ '--verbose', '-v', GetoptLong::NO_ARGUMENT],
61    [ '--timeout', '-t', GetoptLong::OPTIONAL_ARGUMENT],
62    [ '--logfreq', '-l', GetoptLong::OPTIONAL_ARGUMENT],
63    [ '--logfile', GetoptLong::REQUIRED_ARGUMENT],
64    [ '--help', '-h', GetoptLong::NO_ARGUMENT]
65)
66
67# defaults before parsing command line
68@verbose = false
69@xmldata = ""
70@timeout = 10
71@logfreq = 1800
72xmlfile = ""
73logfile = ""
74
75opts.each { |opt, arg|
76  case opt
77    when '--help'
78      begin
79        require 'rdoc/ri/ri_paths'
80        require 'rdoc/usage'
81        RDoc::usage
82      rescue Exception => e
83        puts("Install RDoc::usage or view the comments in the top of the script to get detailed help")
84      end
85
86      exit
87    when '--xml'
88      xmlfile = arg
89    when '--verbose'
90      @verbose = true
91    when '--maxclients'
92      @maxclients = arg
93    when '--logfreq'
94      @logfreq = arg
95    when '--timeout'
96      @timeout = arg
97    when '--logfile'
98      logfile = arg
99  end
100}
101
102# Read the xml data into a string
103if (xmlfile.length > 0 and File.exists?(xmlfile))
104  begin
105    @xmldata = IO.read(xmlfile)
106  rescue Exception => e
107    puts "Got exception #{e.class} #{e} while reading #{xmlfile}"
108    exit
109  end
110else
111  puts("Pass the path to the xml file to serve using --xml, see --help for detailed help")
112  exit
113end
114
115# create a logger keeping 10 files of 1MB each
116begin
117  @logger = Logger.new(logfile, 10, 102400)
118rescue Exception => e
119  puts("Got #{e.class} #{e} while attempting to create logfile #{logfile}")
120  exit
121end
122
123
124class PolicyServer
125  # Generic logging method that takes a severity constant from the Logger class such as Logger::DEBUG
126  def log(severity, msg)
127    @logger.add(severity) { "#{Thread.current.object_id}: #{msg}" }
128  end
129 
130  # Log a msg at level INFO
131  def info(msg)
132    log(Logger::INFO, msg)
133  end
134 
135  # Log a msg at level WARN
136  def warn(msg)
137    log(Logger::WARN, msg)
138  end
139 
140  # Log a msg at level DEBUG
141  def debug(msg)
142    log(Logger::DEBUG, msg)
143  end
144 
145  # Log a msg at level FATAL
146  def fatal(msg)
147    log(Logger::FATAL, msg)
148  end
149 
150  # Log a msg at level ERROR
151  def error(msg)
152    log(Logger::ERROR, msg)
153  end
154 
155  # === Synopsis
156  # Initializes the server
157  #
158  # === Args
159  # +port+::
160  #   The port to listen on, if the port is < 1024 server must run as roo
161  # +host+::
162  #   The host to listen on, use 0.0.0.0 for all addresses
163  # +xml+::
164  #   The XML to serve to clients
165  # +logger+::
166  #   An instanse of the Ruby Standard Logger class
167  # +timeout+::
168  #   How long does client have to complete the whole process before the socket closes
169  #   and the thread terminates
170  # +debug+::
171  #   Set to true to enable DEBUG level logging from startup
172  def initialize(port, host, xml, logger, timeout=10, debug=false)
173    @logger = logger
174    @connections = []
175    @@connMutex = Mutex.new
176    @@clientsMutex = Mutex.new
177    @@bogusclients = 0
178    @@totalclients = 0
179    @timeout = timeout
180    @@starttime = Time.new
181    @xml = xml
182    @port = port
183    @host = host
184
185    if debug
186      @logger.level = Logger::DEBUG
187      debug("Starting in DEBUG mode")
188    else
189      @logger.level = Logger::INFO
190    end
191  end
192 
193  # If the logger instanse is in DEBUG mode, put it into INFO and vica versa
194  def toggledebug
195    if (@logger.debug?)
196      @logger.level = Logger::INFO
197      info("Set logging level to INFO")
198    else
199      @logger.level = Logger::DEBUG
200      info("Set logging level to DEBUG")
201    end
202  end
203 
204  # Walks the list of active connections and dump them to the logger at INFO level
205  def dumpconnections
206    if (@connections.size == 0)
207      info("No active connections to dump")
208    else 
209      connections = @connections
210     
211      info("Dumping current #{connections.size} connections:")
212   
213      connections.each{ |c|
214        addr = c.addr
215        info("#{c.thread.object_id} started at #{c.timecreated} currently in #{c.thread.status} status serving #{addr[2]} [#{addr[3]}]")
216      }
217    end
218  end
219
220  # Dump the current thread list
221  def dumpthreads 
222    Thread.list.each {|t|
223      info("Thread: #{t.id} status #{t.status}")
224    }
225  end
226
227  # Prints some basic stats about the server so far, bogus client are ones that timeout or otherwise cause problems
228  def printstats
229    u = sec2dhms(Time.new - @@starttime)
230   
231    info("Had #{@@totalclients} clients and #{@@bogusclients} bogus clients. Uptime #{u[0]} days #{u[1]} hours #{u[2]} min. #{@connections.size} connection(s) in use now.")
232  end
233 
234  # Logs a message passed to it and increment the bogus client counter inside a mutex
235  def bogusclient(msg, client)
236    addr = client.addr
237   
238    warn("Client #{addr[2]} #{msg}")
239
240    @@clientsMutex.synchronize {
241      @@bogusclients += 1
242    }
243  end
244 
245  # The main logic of client handling, waits for @timeout seconds to receive a null terminated
246  # request containing "policy-file-request" and sends back the data, else marks the client as
247  # bogus and close the connection.
248  #
249  # Any exception caught during this should mark a client as bogus
250  def serve(connection)
251    client = connection.client
252       
253    # Flash clients send a null terminate request
254    $/ = "\000"
255
256    # run this in a timeout block, clients will have --timeout seconds to complete the transaction or go away
257    begin
258      timeout(@timeout.to_i) do
259        loop do
260          request = client.gets
261
262          if request =~ /policy-file-request/
263            client.puts(@xml)
264           
265            debug("Sent xml data to client")
266            break
267          end
268        end
269      end
270    rescue Timeout::Error
271      bogusclient("connection timed out after #{@timeout} seconds", connection)
272    rescue Errno::ENOTCONN => e
273      warn("Unexpected disconnection while handling request")
274    rescue Errno::ECONNRESET => e
275      warn("Connection reset by peer")
276    rescue Exception => e
277      bogusclient("Unexpected #{e.class} exception: #{e}", connection)
278    end
279  end
280 
281  # === Synopsis
282  # Starts the main loop of the server and handles connections, logic is more or less:
283  #
284  # 1. Opens the port for listening
285  # 1. Create a new thread so the connection handling happens seperate from the main loop
286  # 1. Create a loop to accept new sessions from the socket, each new sesison gets a new thread
287  # 1. Increment the totalclient variable for stats handling
288  # 1. Create a OpenStruct structure with detail about the current connection and put it in the @connections array
289  # 1. Pass the connection to the serve method for handling
290  # 1. Once handling completes, remove the connection from the active list and close the socket
291  def start
292    begin
293      # Disable reverse lookups, makes it all slow down
294      BasicSocket::do_not_reverse_lookup=true
295      server = TCPServer.new(@host, @port)
296    rescue Exception => e
297      fatal("Can't open server: #{e.class} #{e}")
298      exit
299    end
300   
301    begin
302      @serverThread = Thread.new {
303        while (session = server.accept)
304          Thread.new(session) do |client| 
305            begin 
306              debug("Handling new connection from #{client.peeraddr[2]}, #{Thread.list.size} total threads ")
307
308              @@clientsMutex.synchronize {
309                @@totalclients += 1
310              }
311
312              connection = OpenStruct.new
313              connection.client = client
314              connection.timecreated = Time.new
315              connection.thread = Thread.current
316              connection.addr = client.peeraddr
317         
318              @@connMutex.synchronize {
319                @connections << connection
320                debug("Pushed connection thread to @connections, now #{@connections.size} connections")
321              }
322             
323              debug("Calling serve on connection")
324              serve(connection)
325         
326              client.close
327         
328              @@connMutex.synchronize {
329                @connections.delete(connection)
330                debug("Removed connection from @connections, now #{@connections.size} connections")
331              }
332         
333            rescue Errno::ENOTCONN => e
334              warn("Unexpected disconnection while handling request")
335            rescue Errno::ECONNRESET => e
336              warn("Connection reset by peer")
337            rescue Exception => e
338              error("Unexpected #{e.class} exception while handling client connection: #{e}")
339              error("Unexpected #{e.class} exception while handling client connection: #{e.backtrace.join("\n")}")
340              client.close
341            end # block around main logic
342          end # while
343        end # around Thread.new for client connections
344      } # @serverThread
345    rescue Exception => e
346      fatal("Got #{e.class} exception in main listening thread: #{e}")
347    end
348  end   
349end
350
351# Goes into the background, chdir's to /tmp, and redirect all input/output to null
352# Beginning Ruby p. 489-490
353def daemonize
354  fork do
355    Process.setsid
356    exit if fork
357    Dir.chdir('/tmp')
358    STDIN.reopen('/dev/null')
359    STDOUT.reopen('/dev/null', 'a')
360    STDERR.reopen('/dev/null', 'a')
361
362    trap("TERM") { 
363      @logger.debug("Caught TERM signal") 
364      exit
365    }
366    yield
367  end
368end
369
370# Returns an array of days, hrs, mins and seconds given a second figure
371# The Ruby Way - Page 227
372def sec2dhms(secs)
373  time = secs.round
374  sec = time % 60
375  time /= 60
376 
377  mins = time % 60
378  time /= 60
379
380  hrs = time % 24
381  time /= 24
382
383  days = time
384  [days, hrs, mins, sec]
385end
386
387# Go into the background and initalizes the server, sets up some signal handlers and print stats
388# every @logfreq seconds, any exceptions gets logged and exits the server
389daemonize do
390  begin
391    @logger.info("Starting server on port 843 in process #{$$}")
392   
393    server = PolicyServer.new(843, "0.0.0.0", @xmldata, @logger, @timeout, @verbose)
394    server.start
395
396    # Send HUP to toggle debug mode or not for a running server
397    trap("HUP") {
398      server.toggledebug
399    }   
400
401    # send a USR1 signal for a full connection list dump
402    trap("USR1") { 
403      server.dumpconnections
404      server.printstats
405    }
406   
407    # Send USR2 to dump all threads
408    trap("USR2") {
409      server.dumpthreads
410    }
411   
412    # Cycle and print stats every now and then
413    loop do
414      sleep @logfreq.to_i
415      server.printstats
416    end
417  rescue SystemExit => e
418    @logger.fatal("Shutting down main daemon thread due to: #{e.class} #{e}")
419  rescue Exception => e
420    @logger.fatal("Unexpected exception #{e.class} from main loop: #{e}")
421    @logger.fatal("Unexpected exception #{e.class} from main loop: #{e.backtrace.join("\n")}")
422  end
423 
424  @logger.info("Server process #{$$} shutting down")
425end
Note: See TracBrowser for help on using the repository browser.