ipcauthpipe 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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