ipcauthpipe 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/README +13 -0
- data/bin/ipcauthpipe +6 -0
- data/config.yml +21 -0
- data/ipcauthpipe.gemspec +46 -0
- data/lib/ipcauthpipe.rb +60 -0
- data/lib/ipcauthpipe/handler.rb +23 -0
- data/lib/ipcauthpipe/handler/auth.rb +81 -0
- data/lib/ipcauthpipe/handler/enumerate.rb +14 -0
- data/lib/ipcauthpipe/handler/passwd.rb +14 -0
- data/lib/ipcauthpipe/handler/pre.rb +31 -0
- data/lib/ipcauthpipe/log.rb +22 -0
- data/lib/ipcauthpipe/processor.rb +37 -0
- data/lib/ipcauthpipe/reader.rb +22 -0
- data/lib/models/member.rb +70 -0
- data/lib/models/member_converge.rb +19 -0
- data/test/config.yml +19 -0
- data/test/ipcauthpipe/handler/auth_spec.rb +84 -0
- data/test/ipcauthpipe/handler/pre_spec.rb +50 -0
- data/test/ipcauthpipe/log_spec.rb +37 -0
- data/test/ipcauthpipe/processor_spec.rb +45 -0
- data/test/models/member_converge_spec.rb +25 -0
- data/test/models/member_spec.rb +78 -0
- data/test/spec_helper.rb +23 -0
- metadata +111 -0
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
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
|
data/ipcauthpipe.gemspec
ADDED
@@ -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
|
data/lib/ipcauthpipe.rb
ADDED
@@ -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
|
+
|
data/test/spec_helper.rb
ADDED
@@ -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
|