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 +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
|