1. #!/usr/bin/ruby
  2. # encoding: utf-8
  3.  
  4. =begin
  5. Squid external_acl helper.
  6.  
  7. = NAME
  8.  exteranl acl - Exteranl ACL helper.
  9.  
  10. = Assumptios
  11.  the basic assumption is that squid does good work and the helper just needs
  12.  to do what he is suppose to be with no error checking at all on data types.
  13.  
  14. = How the helper works?
  15.  the helper works based on stdin\stdout comunication between.
  16.  squid sends a requests information based on the proxy admin settings
  17.  and then expects a corresponding response that will answer the basic
  18.  question of "allow or deny?" by the mentioned data\information.
  19.  The answer can be either OK or ERR only!.
  20.  when OK is intercepted by squid as ALLOW and ERR intercepted by squid
  21.  as DENY.
  22.  
  23. = Example squid settings in squid.conf
  24.  external_acl_type ip_db ttl=30 negative_ttl=30 %SRC %DST /opt/helper/external_acl_ip_db.rb
  25.  acl ip_acl external ip_db
  26.  http_access allow ip_acl
  27.  
  28.  The above sends the helper two values space seperated.
  29.  the first is the SRC IP address and the second is the DST ip address.
  30.  also it allows squid to "cache" a result for the same src and dst pairs
  31.  based requests for 30 secs.
  32.  
  33. = Example squid output towards the helper STDIN
  34.  the external acl "%SRC %DST" format sends to the helper the source IP and dst domain as in:
  35.   Sep 28 05:10:10 proxy1 external_acl.rb[21210]: Original request [192.168.10.146 accounts.youtube.com].
  36.   Sep 28 05:10:10 proxy1 external_acl.rb[21211]: Original request [192.168.10.146 ssl.gstatic.com].
  37.   Sep 28 05:10:10 proxy1 external_acl.rb[21211]: response [ERR] for request [192.168.10.146 ssl.gstatic.com].
  38.   Sep 28 05:10:10 proxy1 external_acl.rb[21210]: response [ERR] for request [192.168.10.146 accounts.youtube.com].
  39.   Sep 28 05:10:11 proxy1 external_acl.rb[21210]: Original request [192.168.10.146 ssl.google-analytics.com].
  40.   Sep 28 05:10:11 proxy1 external_acl.rb[21210]: response [ERR] for request [192.168.10.146 ssl.google-analytics.com].
  41.   Sep 28 05:11:29 proxy1 external_acl.rb[21210]: Original request [192.168.10.146 192.168.10.1].
  42.   Sep 28 05:11:29 proxy1 external_acl.rb[21210]: response [ERR] for request [192.168.10.146 192.168.10.1].
  43.  
  44.  the above shows couple requests which shows a CONNECT method request and plain http request.
  45.  The helper gets the SRC ip related to the proxy server client(which in some nat will be the router ADDR)
  46.  and the DST as the domain (which sometimes the IP is being used) of the destination server.
  47.  it's a a very simple PROTOCOL that is meant to do simple things used this basic data that do exists already.
  48.  
  49. = Helper scketch
  50.  The helper should use some DB that can be updated live using an external
  51.  software and there for the squid or helper instance wont need to reload
  52.  or restart in any form from the second it starts-up until..long..
  53.  
  54. == the helper can be base on a simple DBM db like berkely or tokyocabinet
  55.   but since it needs to be also updated live and better to be used with
  56.   ram cache a more robust system can be used which I choose as MONETA
  57.   RUBYGEM.
  58.  
  59. == DB choice
  60.   I have choosen to use key-value DB since I need a DB only for the key
  61.   Lookup which is the IP of the src users and\or url lookup with a pair
  62.   of rating in a 32/64/128 bits long integer when "-127" is the most
  63.   restricted category that should be blocked to all like spam etc.
  64.  
  65. = COPYRIGHT
  66.  Copyright 2013 Eliezer Croitoru <eliezer@ngtech.co.il>.
  67.  
  68.  This helper is an opensource helper written and designed by ME!
  69.  There for anyone that wishes to look at the code and write a program
  70.  based on this code is stricktly forbidden to just look but allowed and
  71.  advised to copy and paste parts of the code if not all for any pupose.
  72.  If you feel these rights of copy and paste or any use that the above
  73.  is allowing you to be a bad person please feel free to contact the CIA.
  74.  
  75. =end
  76.  
  77. require "rubygems"
  78. require "net/http"
  79. require "open-uri"
  80. require 'timeout'
  81. require 'libxml'
  82.  
  83. require 'ipaddr'
  84.  
  85. require 'syslog'
  86.  
  87. require 'moneta'
  88.  
  89. #Moneta based DB that will be used for auth
  90. #$db = Moneta.new(:HashFile, :dir => '/tmp/db/' , :expires => true)
  91. #$db['key'] = {:a => 1, :b => 2} # This will fail since you can only store Strings
  92.  
  93. #$db = Moneta.new(:LevelDB, :dir => '/tmp/db/' , :expires => true)
  94. #$db['key'] = {:a => 1, :b => 2} # This will fail since you can only store Strings
  95.  
  96. def log(msg)
  97.  Syslog.log(Syslog::LOG_ERR, "%s", msg)
  98. end
  99.  
  100. def eval
  101.         request = gets
  102.         if (request && (request.match(/^[0-9]{1,3}\ /)) )
  103.          conc(request)
  104.          return true
  105.         else
  106.          noconc(request)
  107.          return false
  108.         end
  109. end
  110.  
  111. def conc(request)
  112.         response = "ERR"
  113.        
  114.         return if !request
  115.         request = request.split
  116.         if request[0] && request[1]
  117.                 exit if request[1].start_with?("q")
  118.                 log("Original request [#{request.join(" ")}].") if $debug
  119.                                        
  120.                 begin
  121.                   ip = IPAddr.new(request[1])
  122.                 rescue ArgumentError
  123.                         #"invalid address"
  124.                   STDERR.puts "Error while verifying a src ip \"invalid address\""
  125.                   puts "#{request[0]} #{response}"
  126.                   return nil
  127.                 else
  128.                   STDERR.puts "everything ok"
  129.                 end
  130.                
  131.                 #res = $db.get(ip.to_s)
  132.                 #if ipv4 find the client and change response
  133.                 #case that was found,  response = "OK level=-49"
  134.                 #if ipv6 find the client and change response
  135.                 #response = "OK level=-49 "if res
  136.                 log("response [#{response}] for request [#{request.join(" ")}].") if $debug
  137.  
  138.         end
  139.         puts "#{request[0]} #{response}"
  140. end
  141.  
  142.  
  143. def noconc(request)
  144.         response = "ERR"
  145.        
  146.         return if !request
  147.         request = request.split
  148.         if request[0]
  149.             exit if request[0].start_with?("q")
  150.                 log("Original request [#{request.join(" ")}].") if $debug
  151.                                        
  152.                 begin
  153.                   ip = IPAddr.new(request[0])
  154.                 rescue ArgumentError
  155.                         #"invalid address"
  156.                   STDERR.puts "Error while verifying a src ip \"invalid address\""
  157.                   puts response
  158.                   return nil
  159.                 else
  160.                   STDERR.puts "everything ok"
  161.                 end
  162.                
  163.                 #res = $db.get(ip.to_s)
  164.                 #if ipv4 find the client and change response
  165.                 #case that was found,  response = "OK level=-49"
  166.                 #if ipv6 find the client and change response
  167.                 #response = "OK level=-49 "if res
  168.                 log("response [#{response}] for request [#{request.join(" ")}].") if $debug
  169.                                                        
  170.                
  171.         end
  172.         puts response  
  173. end
  174.  
  175. def validr?(request)
  176.   if ["1.8.7"].include? VERSION
  177.     return true
  178.   end
  179.  
  180.   if (request.ascii_only? && request.valid_encoding?)
  181.     return true
  182.   else
  183.     STDERR.puts("errorness line [#{request}]")
  184.     return false
  185.   end
  186. end
  187.  
  188. def main
  189.         Syslog.open('external_acl.rb', Syslog::LOG_PID)
  190.         log("Started")
  191.  
  192.         concurrency_used = eval
  193.  
  194.          if concurrency_used
  195.            while request = gets
  196.                  conc(request) if validr?(request)
  197.            end
  198.          else
  199.            while request = gets
  200.                  noconc(request) if validr?(request)
  201.            end
  202.          end
  203. end
  204.  
  205.  
  206. $debug = true
  207. STDOUT.sync = true
  208. main

Posted by EliezerCroitoru at 28 Sep 2013, 05:31:21 Etc/UTC
Language: ruby