ipcauthpipe 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,13 @@
1
+ This is implementation of Courier's authpipe protocol over Invision Power Board
2
+ and Converge.
3
+
4
+ This gem acts as an interface between Courier's authlib with Invision's members
5
+ database and allows to authenticate with authlib against Invision's DB.
6
+
7
+ To run it you have to take sample config file included in the gem sources and
8
+ modify it to match your environment. Then configure your courier-authlib
9
+ to run ipcauthpipe with full path to your config file as a parameter, i.e.:
10
+ /usr/local/ipcauthpipe /etc/ipcauthpipe-config.yml
11
+
12
+ See more about Courier's authpipe authentication module and protocol at:
13
+ http://www.courier-mta.org/authlib/README_authlib.html#authpipe
data/bin/ipcauthpipe ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/ipcauthpipe'
4
+
5
+ # Start IpcAuthpipe with config from the first CLI argument of with ../config.yml as a default one
6
+ IpcAuthpipe.start( ARGV[0] || "#{File.dirname(__FILE__)}/../config.yml" )
data/config.yml ADDED
@@ -0,0 +1,21 @@
1
+ database:
2
+ adapter: mysql
3
+ database: pokerru_mail
4
+ username: toor
5
+ password: Awycehe2iS
6
+ socket: /tmp/mysql.sock
7
+
8
+ log:
9
+ level: debug
10
+ file: ipcauthpipe.log
11
+
12
+ invision:
13
+ tables_prefix: ibf_
14
+
15
+ mail:
16
+ address_format: %s@poker.ru
17
+ owner_uid: 2001
18
+ owner_name: vmail
19
+ owner_gid: 1000
20
+ owner_group: virtualusers
21
+ home_dir: /tmp/%s
@@ -0,0 +1,46 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "ipcauthpipe"
3
+ s.version = "0.2.7"
4
+ s.date = "2011-06-11"
5
+ s.summary = "Implementation of Courier's authpipe protocol over Invision Power Board / Converge."
6
+ s.description = "ipcauthpipe gem implements Courier's authpipe protocol to interface Courier POP/IMAP server with Invision Power Board / Converge members database."
7
+ s.has_rdoc = false
8
+
9
+ s.author = "Oleg Ivanov"
10
+ s.email = "morhekil@morhekil.net"
11
+ s.homepage = "http://morhekil.net"
12
+
13
+ s.files = ['lib/ipcauthpipe/handler/auth.rb',
14
+ 'lib/ipcauthpipe/handler/enumerate.rb',
15
+ 'lib/ipcauthpipe/handler/passwd.rb',
16
+ 'lib/ipcauthpipe/handler/pre.rb',
17
+ 'lib/ipcauthpipe/handler.rb',
18
+ 'lib/ipcauthpipe/log.rb',
19
+ 'lib/ipcauthpipe/processor.rb',
20
+ 'lib/ipcauthpipe/reader.rb',
21
+ 'lib/models/member.rb',
22
+ 'lib/models/member_converge.rb',
23
+ 'lib/ipcauthpipe.rb',
24
+ 'bin/ipcauthpipe',
25
+ 'ipcauthpipe.gemspec',
26
+ 'README',
27
+ 'config.yml'
28
+ ]
29
+
30
+ s.test_files = ['test/ipcauthpipe/handler/auth_spec.rb',
31
+ 'test/ipcauthpipe/handler/pre_spec.rb',
32
+ 'test/ipcauthpipe/log_spec.rb',
33
+ 'test/ipcauthpipe/processor_spec.rb',
34
+ 'test/models/member_spec.rb',
35
+ 'test/models/member_converge_spec.rb',
36
+ 'test/spec_helper.rb',
37
+ 'test/config.yml'
38
+ ]
39
+
40
+ s.bindir = 'bin'
41
+ s.executables = ['ipcauthpipe']
42
+
43
+ s.require_path = 'lib'
44
+
45
+ s.add_dependency("activerecord", "~> 2.3.8")
46
+ end
@@ -0,0 +1,60 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'active_record'
5
+ require 'ostruct'
6
+
7
+ require 'fileutils'
8
+
9
+ require 'ipcauthpipe/processor'
10
+ require 'ipcauthpipe/handler'
11
+ require 'ipcauthpipe/reader'
12
+ require 'ipcauthpipe/log'
13
+
14
+ module IpcAuthpipe
15
+
16
+ # Failed authentication (unknown username/password) exception class
17
+ class AuthenticationFailed < StandardError
18
+ end
19
+
20
+ class << self
21
+ attr_reader :config
22
+
23
+ # Starts the processing - initializes the configuration and feeds incoming requests
24
+ # to request processor
25
+ def start(cfgfile)
26
+ init cfgfile
27
+ Log::info 'ipcauthpipe is started'
28
+
29
+ ipc = IpcAuthpipe::Processor.new
30
+ while (line = IpcAuthpipe::Reader.getline) do
31
+ reply = ipc.process(line.strip) unless line.strip.empty?
32
+ Log::debug "Reply is: #{reply.inspect}"
33
+ STDOUT.puts reply
34
+ STDOUT.flush
35
+ end
36
+ end
37
+
38
+ # Reads and stores config file and uses it's data to initialize ActiveRecord connection
39
+ def init(cfgfile)
40
+ # Read and parse YAML config
41
+ cfgdata = YAML::load_file(cfgfile)
42
+
43
+ # Create the global config object
44
+ @config = OpenStruct.new cfgdata
45
+
46
+ # Init logger - set up it's level and log file
47
+ IpcAuthpipe::Log.init(@config.log['file'], @config.log['level'])
48
+
49
+ # And init the ActiveRecord
50
+ ActiveRecord::Base.establish_connection(@config.database)
51
+ ActiveRecord::Base.logger = IpcAuthpipe::Log.logger
52
+
53
+ # and require model classes (we can't do it before as we need initialized config to be available)
54
+ require 'models/member_converge'
55
+ require 'models/member'
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,23 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # A basic class that outlines handlers' API.
5
+ # Right now it features only the single process method that accepts command's parameters
6
+ # as an argument and goes on with processing the specific handler's command.
7
+ class Base
8
+
9
+ # Every handler access command's parameters as a string argument
10
+ # and should return as a string it's answer that will be returned back (to STDOUT generally)
11
+ def process(request)
12
+ raise NotImplementedError, 'request processing should be overriden by concrete handlers'
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+ # Concrete handlers are following
20
+ require 'ipcauthpipe/handler/auth'
21
+ require 'ipcauthpipe/handler/pre'
22
+ require 'ipcauthpipe/handler/passwd'
23
+ require 'ipcauthpipe/handler/enumerate'
@@ -0,0 +1,81 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Auth < IpcAuthpipe::Handler::Base
8
+
9
+ # Main point of entry - accepts additional command's parameters
10
+ # (in case of AUTH - number of data bytes following the command)
11
+ # and proceeds with processing the command
12
+ def self.process(request)
13
+ Log.debug "Processing request [#{request}] in AUTH handler"
14
+ auth = Auth.new
15
+ auth.validate auth.getdata(request.to_i)
16
+ end
17
+
18
+ # Reads the given number of bytes from the input stream and splits
19
+ # them up into a hash of parameters ready for further processing
20
+ def getdata(count)
21
+ Log.debug "Reading [#{count}] bytes from input stream"
22
+ payload = Reader::getbytes(count)
23
+ Log.debug "AUTH payload is #{payload}"
24
+ splits = payload.strip.split(/\s+/m)
25
+ raise ArgumentError, 'Invalid AUTH payload' unless splits.size == 4
26
+
27
+ Log.debug "Analyzing splits [#{splits.inspect}]"
28
+ auth_method(splits)
29
+ end
30
+
31
+ # Analyzes splitted AUTH payload and converts splits into hash
32
+ # of :method, :username and :password for LOGIN authentication and
33
+ # :method, :challenge and :response for CRAM-style authentications
34
+ def auth_method(splits)
35
+ result = { :method => splits[1].strip.downcase }
36
+ result.merge!(
37
+ result[:method] == 'login' ?
38
+ { :username => splits[2].strip.split(/\@/)[0], :password => splits[3].strip } :
39
+ { :challenge => splits[2].strip, :response => splits[3].strip }
40
+ )
41
+
42
+ Log.debug "Converted splits into [#{result.inspect}]"
43
+ result
44
+ end
45
+
46
+ # Accepts analyzed AUTH payload hash and delegated processing onto the
47
+ # specific authentication method's handler. In case of not implemented
48
+ # auth method raises NotImplementedError
49
+ def validate(authdata)
50
+ Log.debug "Validating #{authdata.inspect}"
51
+ begin
52
+ # convert auth type name to a handler's symbol
53
+ method_sym = ( 'validate_with_'+authdata[:method].gsub( /[- ]/, '_' ) ).to_sym
54
+ # and raise an error if it's not implemented
55
+ raise NotImplementedError, "Authentication type #{authdata[:method]} is not supported" unless
56
+ self.respond_to?(method_sym)
57
+ # or delegate processing to the handler if it's here
58
+ Log.debug "Delegating validation to #{method_sym.to_s}"
59
+ self.send(method_sym, authdata)
60
+
61
+ rescue NotImplementedError
62
+ # requested authentication type is not supported
63
+ Log.error "Unsupported authentication type requested with #{authdata.inspect}"
64
+ "FAIL\n"
65
+ rescue AuthenticationFailed
66
+ Log.info "Authentication failed for #{authdata.inspect}"
67
+ "FAIL\n"
68
+ end
69
+ end
70
+
71
+ # LOGIN type authentication handler
72
+ def validate_with_login(authdata)
73
+ Log.debug "Authenticating through type LOGIN with #{authdata.inspect}"
74
+ member = Member.find_by_name_and_password(authdata[:username], authdata[:password])
75
+ member.create_homedir # make sure that homedir is created
76
+ member.to_authpipe # and return the details
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,14 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Enumerate < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.warn "Unsupported command ENUMERATE received with request #{request.inspect}"
10
+ "FAIL\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Passwd < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.warn "Unsupported command PASSWD received with request #{request.inspect}"
10
+ "FAIL\n"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ module IpcAuthpipe
2
+ module Handler
3
+
4
+ # AUTH command handler performs actual authentication of user's data.
5
+ # It gets authentication type and parameters from the input stream and
6
+ # responds with FAIL on failure or user data on success
7
+ class Pre < IpcAuthpipe::Handler::Base
8
+ def self.process(request)
9
+ Log.debug "Processing request [#{request}] in AUTH handler"
10
+ handler = Pre.new
11
+ handler.user_details handler.split_request(request)
12
+ end
13
+
14
+ # Splits request into service and username parts, raises
15
+ # ArgumentError if request string is invalid
16
+ def split_request(request)
17
+ raise ArgumentError, "Invalid PRE request #{request.inspect}" unless /^\. (\w+) (\w+)$/.match( request )
18
+ { :service => $~[1], :username => $~[2] }
19
+ end
20
+
21
+ # Finds member by his username and dumps his details, returns FAIL if not member were found
22
+ def user_details(request)
23
+ member = Member.find_by_name(request[:username])
24
+ member.create_homedir unless member.nil? # make sure the homedir exists
25
+
26
+ member.nil? ? "FAIL\n" : member.to_authpipe
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+
4
+ module IpcAuthpipe
5
+ module Log
6
+
7
+ class << self
8
+ extend Forwardable
9
+ attr_reader :logger
10
+
11
+ def init(filename, loglevel)
12
+ @logger = Logger.new(filename)
13
+ @logger.level = Logger.const_get(loglevel.upcase)
14
+ @logger.formatter = Logger::Formatter.new
15
+ @logger.info "Logger is started"
16
+ end
17
+
18
+ def_delegators :@logger, :add, :debug, :info, :warn, :error, :fatal, :unknown, :close
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ module IpcAuthpipe
2
+
3
+ # This is the main entry point that accepts incoming request strings,
4
+ # validates and splits them into command/parameters parts and delegates processing
5
+ # to the command's handler
6
+ class Processor
7
+
8
+ # Accepts request as a single string, splits it into parts goes onto delegating.
9
+ # Returns handler's response back to the caller
10
+ def process(request)
11
+ Log.debug "Processing request: #{request.rstrip}"
12
+ begin
13
+ call_handler_for split_request(request)
14
+ rescue Exception => excp
15
+ Log.fatal "#{excp.class}, #{excp.message}"
16
+ raise
17
+ end
18
+ end
19
+
20
+ # Splits request into a command and it's parameters, validating command on the way.
21
+ # Raises RuntimeError on invalid command or (that's actually the same thing) unparsable request
22
+ def split_request(req)
23
+ raise RuntimeError, 'Invalid request received' unless /^(PRE|AUTH|PASSWD|ENUMERATE)(?:$| (.*)$)/.match(req)
24
+ {
25
+ :command => $1,
26
+ :params => $2.nil? || $2.empty? ? nil : $2
27
+ }
28
+ end
29
+
30
+ # Delegates processing to a concrete handler of request's command
31
+ def call_handler_for(request)
32
+ Log.debug "Calling #{request[:command].capitalize} handler with [#{request[:params]}] as a parameter"
33
+ IpcAuthpipe::Handler.const_get(request[:command].capitalize).process(request[:params])
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,22 @@
1
+ require 'readbytes'
2
+
3
+ module IpcAuthpipe
4
+
5
+ # Abstracting read operations to substitute them with mocks in our tests and also
6
+ # to give potentially a way to replace STDIN operation with something different - file based,
7
+ # for example
8
+ class Reader
9
+
10
+ # Returns next line waiting on the input
11
+ def self.getline
12
+ STDIN.gets("\n")
13
+ end
14
+
15
+ # Returns exactly count number of bytes waiting on the input
16
+ def self.getbytes(count)
17
+ STDIN.readbytes(count)
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,70 @@
1
+ class Member < ActiveRecord::Base
2
+ set_table_name IpcAuthpipe::config.invision['tables_prefix'] + 'members'
3
+
4
+ def self.find_by_name_and_password(username, password)
5
+ member = find_by_name(username)
6
+ raise(
7
+ IpcAuthpipe::AuthenticationFailed, 'invalid password'
8
+ ) unless member.kind_of?(Member) && member.valid_password?(password) && member.has_mail_access?
9
+
10
+ member
11
+ end
12
+
13
+ def has_mail_access?
14
+ allowed_group = IpcAuthpipe::config.invision['allowed_group']
15
+ if allowed_group
16
+ # if allowed_group is defined - make sure that user is in this group
17
+ # before allowing him to access email
18
+ groups = [ member_group_id.to_s ] + mgroup_others.split(',')
19
+ groups.include?(allowed_group.to_s)
20
+ else
21
+ # if allowed group is not set - allow all users to use the mail service
22
+ true
23
+ end
24
+ end
25
+
26
+ def homedir
27
+ (IpcAuthpipe::config.mail['home_dir'] % "#{name[0..0]}/#{name}").downcase
28
+ end
29
+
30
+ # Create user's home dir if it's not present
31
+ def create_homedir
32
+ unless File.exists?(homedir)
33
+ FileUtils.mkdir_p(homedir, :mode => 0750)
34
+ FileUtils.mkdir_p("#{homedir}/cur", :mode => 0750)
35
+ FileUtils.mkdir_p("#{homedir}/new", :mode => 0750)
36
+ FileUtils.mkdir_p("#{homedir}/tmp", :mode => 0750)
37
+ FileUtils.chown(IpcAuthpipe::config.mail['owner_name'], IpcAuthpipe::config.mail['owner_group'], "#{homedir}/..")
38
+ FileUtils.chown_R(IpcAuthpipe::config.mail['owner_name'], IpcAuthpipe::config.mail['owner_group'], homedir)
39
+ end
40
+ end
41
+
42
+ def to_authpipe
43
+ IpcAuthpipe::Log.debug "Dumping authpipe string for member data #{inspect}"
44
+ stringdump = [
45
+ "UID=#{IpcAuthpipe::config.mail['owner_uid']}",
46
+ "GID=#{IpcAuthpipe::config.mail['owner_gid']}",
47
+ "HOME=#{homedir}/",
48
+ "MAILDIR=#{homedir}/",
49
+ "ADDRESS=#{(IpcAuthpipe::config.mail['address_format'] % name).downcase}",
50
+ "."
51
+ ].join("\n")+"\n"
52
+ IpcAuthpipe::Log.debug "Authpipe dump: #{stringdump.inspect}"
53
+
54
+ stringdump
55
+ end
56
+
57
+ # Verifies if the given clear password matches hash and salt stored in IPB's database,
58
+ # returns true/false depending on the result
59
+ def valid_password?(cleartext)
60
+ return salted_hash(cleartext) == members_pass_hash
61
+ end
62
+
63
+ # Calculates and returns IPB-style salted hash for a given text string
64
+ def salted_hash(text)
65
+ return Digest::MD5.hexdigest(
66
+ Digest::MD5.hexdigest(members_pass_salt) + Digest::MD5.hexdigest(text)
67
+ )
68
+ end
69
+
70
+ end
@@ -0,0 +1,19 @@
1
+ require 'digest/md5'
2
+
3
+ class MemberConverge < ActiveRecord::Base
4
+ set_table_name IpcAuthpipe::config.invision['tables_prefix'] + 'members_converge'
5
+ set_primary_key 'converge_id'
6
+
7
+ # Verifies if the given clear password matches hash and salt stored in IPB's database,
8
+ # returns true/false depending on the result
9
+ def valid_password?(cleartext)
10
+ return salted_hash(cleartext) == converge_pass_hash
11
+ end
12
+
13
+ # Calculates and returns IPB-style salted hash for a given text string
14
+ def salted_hash(text)
15
+ return Digest::MD5.hexdigest(
16
+ Digest::MD5.hexdigest(converge_pass_salt) + Digest::MD5.hexdigest(text)
17
+ )
18
+ end
19
+ end
data/test/config.yml ADDED
@@ -0,0 +1,19 @@
1
+ database:
2
+ adapter: mysql
3
+ database: pokerru_mail_test
4
+ username: toor
5
+ password: Awycehe2iS
6
+ socket: /tmp/mysql.sock
7
+
8
+ log:
9
+ level: debug
10
+ file: ipcauthpipe_test.log
11
+
12
+ invision:
13
+ tables_prefix: ibf_
14
+
15
+ mail:
16
+ address_format: %s@poker.ru
17
+ owner_uid: 2000
18
+ owner_gid: 1000
19
+ home_dir: /home/vmail/%s
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ require 'ipcauthpipe/handler'
4
+ require 'ipcauthpipe/handler/auth'
5
+ require 'ipcauthpipe/reader'
6
+ require 'models/member'
7
+
8
+ describe "AUTH handler" do
9
+
10
+ before(:all) do
11
+ IpcAuthpipe::Log.logger = stub_everything
12
+ end
13
+
14
+ before(:each) do
15
+ @auth = IpcAuthpipe::Handler::Auth.new
16
+ end
17
+
18
+ it "should read given number of bytes from the input stream and pass them onto the analyzer splitted into an array" do
19
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(29).and_return("imap \nlogin \n vasya \n parol ")
20
+ # we're expecting array as an argument for auth_method
21
+ @auth.should_receive(:auth_method).once.with( duck_type(:is_array) )
22
+ @auth.getdata(29)
23
+ end
24
+
25
+ it "should raise ArgumentError exception on invalid payload" do
26
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(33).and_return("imap \nlogin \n vasya \n parol \n wtf")
27
+ lambda { @auth.getdata(33) }.should raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should properly handle both CR-delimited and non-CR-delimited payloads" do
31
+ @auth.should_receive(:auth_method).twice.with(['imap', 'login', 'vasya', 'parol'])
32
+
33
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(29).and_return("imap \nlogin \n vasya \n parol ")
34
+ @auth.getdata(29)
35
+
36
+ IpcAuthpipe::Reader.should_receive(:getbytes).once.with(30).and_return("imap \nlogin \n vasya \n parol \n")
37
+ @auth.getdata(30)
38
+ end
39
+
40
+ it "should convert LOGIN's payload into a hash with username and password" do
41
+ @auth.auth_method( ['imap', 'login', 'vasya', 'parol'] ).should == {
42
+ :method => 'login', :username => 'vasya', :password => 'parol'
43
+ }
44
+ end
45
+
46
+ it "should convert CRAM's payload into a hash with challenge and response" do
47
+ @auth.auth_method( ['imap', 'cram-md5', 'vasya', 'parol'] ).should == {
48
+ :method => 'cram-md5', :challenge => 'vasya', :response => 'parol'
49
+ }
50
+ end
51
+
52
+ it "should return FAIL for unsupported authentication types" do
53
+ @auth.validate(:method => 'foobar').should == "FAIL\n"
54
+ end
55
+
56
+ it "should delegate processing onto a handler for supported authentication types" do
57
+ authdata = { :method => 'login', :username => 'vasya', :password => 'parol' }
58
+ @auth.should_receive(:validate_with_login).once.with(authdata).and_return('LOGIN-success')
59
+ @auth.validate(authdata).should == "LOGIN-success"
60
+ end
61
+
62
+ it "should return FAIL for failed authentication" do
63
+ authdata = { :method => 'login', :username => 'vasya', :password => 'parol' }
64
+ @auth.should_receive(:validate_with_login).once.with(authdata).and_raise(IpcAuthpipe::AuthenticationFailed)
65
+ @auth.validate(authdata).should == "FAIL\n"
66
+ end
67
+
68
+ describe "with LOGIN auth type" do
69
+
70
+ it "should find member and return it formatted" do
71
+ member = mock('member')
72
+ Member.should_receive(:find_by_name_and_password).with('vasya', 'parol').once.and_return(member)
73
+ member.should_receive(:to_authpipe).once.and_return("TEXT\nDUMP\nUSER\n.")
74
+
75
+ @auth.validate_with_login(:username => 'vasya', :password => 'parol').should == "TEXT\nDUMP\nUSER\n."
76
+ end
77
+
78
+ end
79
+
80
+
81
+ # it "should detect authentication method being used" do
82
+ # IpcAuthpipe::Handler::Auth.
83
+ # end
84
+ end
@@ -0,0 +1,50 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ require 'ipcauthpipe/handler'
4
+ require 'ipcauthpipe/handler/auth'
5
+ require 'ipcauthpipe/reader'
6
+ require 'models/member'
7
+
8
+ describe "PRE handler" do
9
+
10
+ before(:all) do
11
+ IpcAuthpipe::Log.logger = stub_everything
12
+ end
13
+
14
+ before(:each) do
15
+ @pre = IpcAuthpipe::Handler::Pre.new
16
+ end
17
+
18
+ it "should split request and find a member" do
19
+ request = '. imap tester'
20
+ splitted_request = { :service => 'imap', :username => 'tester' }
21
+ @pre.should_receive(:split_request).with(request).once.and_return(splitted_request)
22
+ @pre.should_receive(:user_details).with(splitted_request).once.and_return("MEMBER\nDUMP\n.")
23
+ IpcAuthpipe::Handler::Pre.should_receive(:new).once.and_return(@pre)
24
+
25
+ IpcAuthpipe::Handler::Pre.process(request)
26
+ end
27
+
28
+ it "should split request into authservice and username parts" do
29
+ @pre.split_request('. pop3 tester').should == { :service => 'pop3', :username => 'tester' }
30
+ end
31
+
32
+ it "should raise ArgumentError if request string is invalid" do
33
+ lambda { @pre.split_request('wtfisthis') }.should raise_error(ArgumentError)
34
+ end
35
+
36
+ it "should find and return user's info by his name" do
37
+ member = mock('member')
38
+ Member.should_receive(:find_by_name).once.with('tester').and_return(member)
39
+ member.should_receive(:to_authpipe).once.and_return("MEMBER\nINFO\n.")
40
+
41
+ request = { :service => 'pop3', :username => 'tester' }
42
+ @pre.user_details(request).should == "MEMBER\nINFO\n."
43
+ end
44
+
45
+ it "should return FAIL for invalid username" do
46
+ Member.should_receive(:find_by_name).once.with('foobar').and_return(nil)
47
+ request = { :service => 'pop3', :username => 'foobar' }
48
+ @pre.user_details(request).should == "FAIL\n"
49
+ end
50
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'ipcauthpipe/log'
4
+
5
+ describe 'Log' do
6
+
7
+ before(:each) do
8
+ # stubbing new instance and returning our mock instead
9
+ @logger = mock('log_instance', :level= => nil, :info => nil, :formatter= => nil)
10
+ Logger.should_receive(:new).with('test.log').once.and_return(@logger)
11
+ end
12
+
13
+ it "should init logger with given filename and log level" do
14
+ # log level should be set to debug
15
+ @logger.should_receive(:level=).with(Logger::DEBUG).once
16
+
17
+ # formatter should be initialized with our mock
18
+ formatter = mock('log_formatter')
19
+ Logger::Formatter.should_receive(:new).once.and_return(formatter)
20
+ @logger.should_receive(:formatter=).once.with(formatter)
21
+
22
+ # and info message should be logged
23
+ @logger.should_receive(:info).once
24
+
25
+ IpcAuthpipe::Log.init('test.log', 'debug')
26
+ end
27
+
28
+ it "should delegate all common logging methods to stdlib's logger" do
29
+ IpcAuthpipe::Log.init('test.log', 'debug')
30
+
31
+ [ :debug, :info, :warn, :error, :fatal, :add, :unknown].each do |msg|
32
+ @logger.should_receive(msg).with('test').once
33
+ IpcAuthpipe::Log.send(msg, 'test')
34
+ end
35
+ end
36
+
37
+ end
@@ -0,0 +1,45 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'ipcauthpipe/processor'
4
+
5
+ describe 'Request processor' do
6
+
7
+ before(:each) do
8
+ @processor = IpcAuthpipe::Processor.new
9
+ end
10
+
11
+ it "should split a request into a command and it's parameters" do
12
+ @processor.split_request('PRE . pop3 myname').should == { :command => 'PRE', :params => '. pop3 myname'}
13
+ @processor.split_request('ENUMERATE').should == { :command => 'ENUMERATE', :params => nil}
14
+ @processor.split_request('ENUMERATE ').should == { :command => 'ENUMERATE', :params => nil}
15
+ end
16
+
17
+ it "should raise an exception for invalid request and log it" do
18
+ lambda { @processor.split_request('FOOBAR') }.should raise_error(RuntimeError)
19
+ lambda { @processor.split_request('NO . COMMAND') }.should raise_error(RuntimeError)
20
+ end
21
+
22
+ it "should log failed requests" do
23
+ IpcAuthpipe::Log.should_receive(:debug).once
24
+ IpcAuthpipe::Log.should_receive(:fatal).once
25
+ lambda { @processor.process('FOOBAR') }.should raise_error(RuntimeError)
26
+ end
27
+
28
+ it "should delegate processing to the command's handler" do
29
+ # Set up a fake Auth handler
30
+ module IpcAuthpipe
31
+ module Handler
32
+ class Auth
33
+ def self.process
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # Expect the action to be logged
40
+ IpcAuthpipe::Log.should_receive(:debug).once
41
+ # And try to call it
42
+ IpcAuthpipe::Handler::Auth.should_receive(:process).with('53').once
43
+ @processor.call_handler_for(:command => 'AUTH', :params => '53')
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'active_record'
4
+ require 'models/member_converge'
5
+
6
+ describe 'Member Converge' do
7
+
8
+ before(:each) do
9
+ salt = '/yU(t'
10
+ password = 'testtest'
11
+ @converge = MemberConverge.new(
12
+ # password is testtest
13
+ :converge_pass_hash => Digest::MD5.hexdigest(
14
+ Digest::MD5.hexdigest(salt) + Digest::MD5.hexdigest(password)
15
+ ),
16
+ :converge_pass_salt => salt
17
+ )
18
+ end
19
+
20
+ it "should verify cleartext password against hashed one" do
21
+ @converge.valid_password?('foobar').should == false
22
+ @converge.valid_password?('testtest').should == true
23
+ end
24
+
25
+ end
@@ -0,0 +1,78 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ require 'active_record'
4
+ require 'models/member'
5
+ require 'models/member_converge'
6
+
7
+ describe Member do
8
+
9
+ before(:all) do
10
+ IpcAuthpipe::Log.logger = stub('stub').as_null_object
11
+ end
12
+
13
+ before(:each) do
14
+ Member.delete_all
15
+ @tester = mock('member', :kind_of? => true) #,
16
+ @member = Member.create(:email => 'test@test.com', :name => 'tester')
17
+ #:pass_hash => '3a063c7f0d62df2ff444ca22455a7232', :pass_salt => '/yU(t') # password is 'testtest'
18
+ end
19
+
20
+ it "should find a member by his username and cleartext password" do
21
+ Member.should_receive(:find_by_name).once.with(@member.name).and_return(@tester)
22
+ @tester.should_receive(:valid_password?).with('testtest').once.and_return(true)
23
+ @tester.should_receive(:has_mail_access?).once.and_return(true)
24
+
25
+ Member.find_by_name_and_password( @member.name, 'testtest' ).should == @tester
26
+ end
27
+
28
+ it "should raise AuthenticationFailed exception on invalid password" do
29
+ Member.should_receive(:find).once.and_return(@tester)
30
+ @tester.should_receive(:valid_password?).with('wrongpassword').once.and_return(false)
31
+
32
+ lambda { Member.find_by_name_and_password( @member.name, 'wrongpassword' ) }.should raise_error(IpcAuthpipe::AuthenticationFailed)
33
+ end
34
+
35
+ it "should raise AuthenticationFailed exception on invalid username" do
36
+ MemberConverge.should_receive(:find).never
37
+ @tester.should_receive(:valid_password?).never
38
+
39
+ lambda { Member.find_by_name_and_password( 'foobarname', 'wrongpassword' ) }.should raise_error(IpcAuthpipe::AuthenticationFailed)
40
+ end
41
+
42
+ it "should dump itself into text string for authlib" do
43
+ member = Member.new(
44
+ :name => 'Tester'
45
+ )
46
+
47
+ member.to_authpipe.should == [
48
+ "UID=#{IpcAuthpipe::config.mail['owner_uid']}",
49
+ "GID=#{IpcAuthpipe::config.mail['owner_gid']}",
50
+ "HOME=/home/vmail/t/tester/",
51
+ "MAILDIR=/home/vmail/t/tester/",
52
+ "ADDRESS=tester@poker.ru",
53
+ "."
54
+ ].join("\n") + "\n"
55
+ end
56
+
57
+ describe 'password validation' do
58
+
59
+ before(:each) do
60
+ salt = '/yU(t'
61
+ password = 'testtest'
62
+ @member = Member.new(
63
+ # password is testtest
64
+ :members_pass_hash => Digest::MD5.hexdigest(
65
+ Digest::MD5.hexdigest(salt) + Digest::MD5.hexdigest(password)
66
+ ),
67
+ :members_pass_salt => salt
68
+ )
69
+ end
70
+
71
+ it "should verify cleartext password against hashed one" do
72
+ @member.valid_password?('foobar').should == false
73
+ @member.valid_password?('testtest').should == true
74
+ end
75
+
76
+ end
77
+ end
78
+
@@ -0,0 +1,23 @@
1
+ # $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+ #require 'rubygems'
4
+ #require 'bacon'
5
+ #require 'github_post_receive_server'
6
+
7
+ require 'ipcauthpipe'
8
+ require 'ipcauthpipe/log'
9
+ # stubbing out Log's methods
10
+ module IpcAuthpipe::Log
11
+ class << self
12
+ attr_accessor :logger
13
+ end
14
+ end
15
+ # and init test config
16
+ IpcAuthpipe.init('test/config.yml')
17
+
18
+ # helper method for Array class to use in rspec's duck_type expectation
19
+ class Array
20
+ def is_array
21
+ true
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ipcauthpipe
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 7
10
+ version: 0.2.7
11
+ platform: ruby
12
+ authors:
13
+ - Oleg Ivanov
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-11 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 8
34
+ version: 2.3.8
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: ipcauthpipe gem implements Courier's authpipe protocol to interface Courier POP/IMAP server with Invision Power Board / Converge members database.
38
+ email: morhekil@morhekil.net
39
+ executables:
40
+ - ipcauthpipe
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - lib/ipcauthpipe/handler/auth.rb
47
+ - lib/ipcauthpipe/handler/enumerate.rb
48
+ - lib/ipcauthpipe/handler/passwd.rb
49
+ - lib/ipcauthpipe/handler/pre.rb
50
+ - lib/ipcauthpipe/handler.rb
51
+ - lib/ipcauthpipe/log.rb
52
+ - lib/ipcauthpipe/processor.rb
53
+ - lib/ipcauthpipe/reader.rb
54
+ - lib/models/member.rb
55
+ - lib/models/member_converge.rb
56
+ - lib/ipcauthpipe.rb
57
+ - bin/ipcauthpipe
58
+ - ipcauthpipe.gemspec
59
+ - README
60
+ - config.yml
61
+ - test/ipcauthpipe/handler/auth_spec.rb
62
+ - test/ipcauthpipe/handler/pre_spec.rb
63
+ - test/ipcauthpipe/log_spec.rb
64
+ - test/ipcauthpipe/processor_spec.rb
65
+ - test/models/member_spec.rb
66
+ - test/models/member_converge_spec.rb
67
+ - test/spec_helper.rb
68
+ - test/config.yml
69
+ has_rdoc: true
70
+ homepage: http://morhekil.net
71
+ licenses: []
72
+
73
+ post_install_message:
74
+ rdoc_options: []
75
+
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ hash: 3
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ requirements: []
97
+
98
+ rubyforge_project:
99
+ rubygems_version: 1.6.2
100
+ signing_key:
101
+ specification_version: 3
102
+ summary: Implementation of Courier's authpipe protocol over Invision Power Board / Converge.
103
+ test_files:
104
+ - test/ipcauthpipe/handler/auth_spec.rb
105
+ - test/ipcauthpipe/handler/pre_spec.rb
106
+ - test/ipcauthpipe/log_spec.rb
107
+ - test/ipcauthpipe/processor_spec.rb
108
+ - test/models/member_spec.rb
109
+ - test/models/member_converge_spec.rb
110
+ - test/spec_helper.rb
111
+ - test/config.yml