source: Serveur/flashpolicyd/flashpolicyd-2.1/flashpolicyd.rb @ 2850

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

Pas mal de modifications pour le faire fonctionner correctement sur debian

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