See #730. Forking and adding Postgres-PR, with the following changes:

Namespaced everything under Msf::Db::PostgreSQL, renamed top-level include to postgres_msf to disambiguate.
Included recursive requires for all files.
Noted the IO monkeypatch -- should revisit.
Added a testcase for database connections.

The reason for the namespacing is to avoid stomping on any existing Postgres-PR installations, or any other requires named "postgres" or "postgresql" or even "pg," since these may or may not support the method's we're using here. The seperate namespace also allows for easier integration of custom commands later on.




git-svn-id: file:///home/svn/framework3/trunk@8342 4d416f70-5f16-0410-b530-b9f4589650da
unstable
Tod Beardsley 2010-02-01 19:49:36 +00:00
parent 554f46be43
commit a65af9c8b3
13 changed files with 1429 additions and 0 deletions

View File

@ -0,0 +1,128 @@
require 'postgres_msf'
require 'postgres/byteorder'
# Namespace for Metasploit branch.
module Msf
module Db
# This mixin solely depends on method read(n), which must be defined
# in the class/module where you mixin this module.
module BinaryReaderMixin
# == 8 bit
# no byteorder for 8 bit!
def read_word8
ru(1, 'C')
end
def read_int8
ru(1, 'c')
end
alias read_byte read_word8
# == 16 bit
# === Unsigned
def read_word16_native
ru(2, 'S')
end
def read_word16_little
ru(2, 'v')
end
def read_word16_big
ru(2, 'n')
end
# === Signed
def read_int16_native
ru(2, 's')
end
def read_int16_little
# swap bytes if native=big (but we want little)
ru_swap(2, 's', ByteOrder::Big)
end
def read_int16_big
# swap bytes if native=little (but we want big)
ru_swap(2, 's', ByteOrder::Little)
end
# == 32 bit
# === Unsigned
def read_word32_native
ru(4, 'L')
end
def read_word32_little
ru(4, 'V')
end
def read_word32_big
ru(4, 'N')
end
# === Signed
def read_int32_native
ru(4, 'l')
end
def read_int32_little
# swap bytes if native=big (but we want little)
ru_swap(4, 'l', ByteOrder::Big)
end
def read_int32_big
# swap bytes if native=little (but we want big)
ru_swap(4, 'l', ByteOrder::Little)
end
# == Aliases
alias read_uint8 read_word8
# add some short-cut functions
%w(word16 int16 word32 int32).each do |typ|
alias_method "read_#{typ}_network", "read_#{typ}_big"
end
{:word16 => :uint16, :word32 => :uint32}.each do |old, new|
['_native', '_little', '_big', '_network'].each do |bo|
alias_method "read_#{new}#{bo}", "read_#{old}#{bo}"
end
end
# read exactly n characters, otherwise raise an exception.
def readn(n)
str = read(n)
raise "couldn't read #{n} characters" if str.nil? or str.size != n
str
end
private
# shortcut method for readn+unpack
def ru(size, template)
readn(size).unpack(template).first
end
# same as method +ru+, but swap bytes if native byteorder == _byteorder_
def ru_swap(size, template, byteorder)
str = readn(size)
str.reverse! if ByteOrder.byteorder == byteorder
str.unpack(template).first
end
end
end
end

View File

@ -0,0 +1,108 @@
require 'postgres_msf'
require 'postgres/byteorder'
# Namespace for Metasploit branch.
module Msf
module Db
module BinaryWriterMixin
# == 8 bit
# no byteorder for 8 bit!
def write_word8(val)
pw(val, 'C')
end
def write_int8(val)
pw(val, 'c')
end
alias write_byte write_word8
# == 16 bit
# === Unsigned
def write_word16_native(val)
pw(val, 'S')
end
def write_word16_little(val)
str = [val].pack('S')
str.reverse! if ByteOrder.network? # swap bytes as native=network (and we want little)
write(str)
end
def write_word16_network(val)
str = [val].pack('S')
str.reverse! if ByteOrder.little? # swap bytes as native=little (and we want network)
write(str)
end
# === Signed
def write_int16_native(val)
pw(val, 's')
end
def write_int16_little(val)
pw(val, 'v')
end
def write_int16_network(val)
pw(val, 'n')
end
# == 32 bit
# === Unsigned
def write_word32_native(val)
pw(val, 'L')
end
def write_word32_little(val)
str = [val].pack('L')
str.reverse! if ByteOrder.network? # swap bytes as native=network (and we want little)
write(str)
end
def write_word32_network(val)
str = [val].pack('L')
str.reverse! if ByteOrder.little? # swap bytes as native=little (and we want network)
write(str)
end
# === Signed
def write_int32_native(val)
pw(val, 'l')
end
def write_int32_little(val)
pw(val, 'V')
end
def write_int32_network(val)
pw(val, 'N')
end
# add some short-cut functions
%w(word16 int16 word32 int32).each do |typ|
alias_method "write_#{typ}_big", "write_#{typ}_network"
end
# == Other methods
private
# shortcut for pack and write
def pw(val, template)
write([val].pack(template))
end
end
end
end

105
lib/postgres/buffer.rb Normal file
View File

@ -0,0 +1,105 @@
require 'postgres_msf'
require 'postgres/binary_writer'
require 'postgres/binary_reader'
# Namespace for Metasploit branch.
module Msf
module Db
# Fixed size buffer.
class Buffer
class Error < RuntimeError; end
class EOF < Error; end
def self.from_string(str)
new(str)
end
def self.of_size(size)
raise ArgumentError if size < 0
new('#' * size)
end
def initialize(content)
@size = content.size
@content = content
@position = 0
end
def size
@size
end
def position
@position
end
def position=(new_pos)
raise ArgumentError if new_pos < 0 or new_pos > @size
@position = new_pos
end
def at_end?
@position == @size
end
def content
@content
end
def read(n)
raise EOF, 'cannot read beyond the end of buffer' if @position + n > @size
str = @content[@position, n]
@position += n
str
end
def write(str)
sz = str.size
raise EOF, 'cannot write beyond the end of buffer' if @position + sz > @size
@content[@position, sz] = str
@position += sz
self
end
def copy_from_stream(stream, n)
raise ArgumentError if n < 0
while n > 0
str = stream.read(n)
write(str)
n -= str.size
end
raise if n < 0
end
NUL = "\000"
def write_cstring(cstr)
raise ArgumentError, "Invalid Ruby/cstring" if cstr.include?(NUL)
write(cstr)
write(NUL)
end
# returns a Ruby string without the trailing NUL character
def read_cstring
nul_pos = @content.index(NUL, @position)
raise Error, "no cstring found!" unless nul_pos
sz = nul_pos - @position
str = @content[@position, sz]
@position += sz + 1
return str
end
# read till the end of the buffer
def read_rest
read(self.size-@position)
end
include BinaryWriterMixin
include BinaryReaderMixin
end
end
end

41
lib/postgres/byteorder.rb Normal file
View File

@ -0,0 +1,41 @@
require 'postgres_msf'
# Namespace for Metasploit branch.
module Msf
module Db
module ByteOrder
Native = :Native
BigEndian = Big = Network = :BigEndian
LittleEndian = Little = :LittleEndian
# examines the byte order of the underlying machine
def byte_order
if [0x12345678].pack("L") == "\x12\x34\x56\x78"
BigEndian
else
LittleEndian
end
end
alias byteorder byte_order
def little_endian?
byte_order == LittleEndian
end
def big_endian?
byte_order == BigEndian
end
alias little? little_endian?
alias big? big_endian?
alias network? big_endian?
module_function :byte_order, :byteorder
module_function :little_endian?, :little?
module_function :big_endian?, :big?, :network?
end
end
end

View File

@ -0,0 +1,182 @@
#
# Author:: Michael Neumann
# Copyright:: (c) 2005 by Michael Neumann
# License:: Same as Ruby's or BSD
#
require 'postgres_msf'
require 'postgres/postgres-pr/message'
require 'postgres/postgres-pr/version'
require 'uri'
require 'socket'
# Namespace for Metasploit branch.
module Msf
module Db
module PostgresPR
PROTO_VERSION = 3 << 16 #196608
class Connection
# A block which is called with the NoticeResponse object as parameter.
attr_accessor :notice_processor
#
# Returns one of the following statuses:
#
# PQTRANS_IDLE = 0 (connection idle)
# PQTRANS_INTRANS = 2 (idle, within transaction block)
# PQTRANS_INERROR = 3 (idle, within failed transaction)
# PQTRANS_UNKNOWN = 4 (cannot determine status)
#
# Not yet implemented is:
#
# PQTRANS_ACTIVE = 1 (command in progress)
#
def transaction_status
case @transaction_status
when ?I
0
when ?T
2
when ?E
3
else
4
end
end
def initialize(database, user, password=nil, uri = nil)
uri ||= DEFAULT_URI
@transaction_status = nil
@params = {}
establish_connection(uri)
@conn << StartupMessage.new(PROTO_VERSION, 'user' => user, 'database' => database).dump
loop do
msg = Message.read(@conn)
case msg
when AuthentificationClearTextPassword
raise ArgumentError, "no password specified" if password.nil?
@conn << PasswordMessage.new(password).dump
when AuthentificationCryptPassword
raise ArgumentError, "no password specified" if password.nil?
@conn << PasswordMessage.new(password.crypt(msg.salt)).dump
when AuthentificationMD5Password
raise ArgumentError, "no password specified" if password.nil?
require 'digest/md5'
m = Digest::MD5.hexdigest(password + user)
m = Digest::MD5.hexdigest(m + msg.salt)
m = 'md5' + m
@conn << PasswordMessage.new(m).dump
when AuthentificationKerberosV4, AuthentificationKerberosV5, AuthentificationSCMCredential
raise "unsupported authentification"
when AuthentificationOk
when ErrorResponse
raise msg.field_values.join("\t")
when NoticeResponse
@notice_processor.call(msg) if @notice_processor
when ParameterStatus
@params[msg.key] = msg.value
when BackendKeyData
# TODO
#p msg
when ReadyForQuery
@transaction_status = msg.backend_transaction_status_indicator
break
else
raise "unhandled message type"
end
end
end
def close
raise "connection already closed" if @conn.nil?
@conn.shutdown
@conn = nil
end
class Result
attr_accessor :rows, :fields, :cmd_tag
def initialize(rows=[], fields=[])
@rows, @fields = rows, fields
end
end
def query(sql)
@conn << Query.dump(sql)
result = Result.new
errors = []
loop do
msg = Message.read(@conn)
case msg
when DataRow
result.rows << msg.columns
when CommandComplete
result.cmd_tag = msg.cmd_tag
when ReadyForQuery
@transaction_status = msg.backend_transaction_status_indicator
break
when RowDescription
result.fields = msg.fields
when CopyInResponse
when CopyOutResponse
when EmptyQueryResponse
when ErrorResponse
# TODO
errors << msg
when NoticeResponse
@notice_processor.call(msg) if @notice_processor
else
# TODO
end
end
raise errors.map{|e| e.field_values.join("\t") }.join("\n") unless errors.empty?
result
end
DEFAULT_PORT = 5432
DEFAULT_HOST = 'localhost'
DEFAULT_PATH = '/tmp'
DEFAULT_URI =
if RUBY_PLATFORM.include?('win')
'tcp://' + DEFAULT_HOST + ':' + DEFAULT_PORT.to_s
else
'unix:' + File.join(DEFAULT_PATH, '.s.PGSQL.' + DEFAULT_PORT.to_s)
end
private
# tcp://localhost:5432
# unix:/tmp/.s.PGSQL.5432
def establish_connection(uri)
u = URI.parse(uri)
case u.scheme
when 'tcp'
@conn = TCPSocket.new(u.host || DEFAULT_HOST, u.port || DEFAULT_PORT)
when 'unix'
@conn = UNIXSocket.new(u.path)
else
raise 'unrecognized uri scheme format (must be tcp or unix)'
end
end
end
end # module PostgresPR
end
end

View File

@ -0,0 +1,552 @@
#
# Author:: Michael Neumann
# Copyright:: (c) 2005 by Michael Neumann
# License:: Same as Ruby's or BSD
#
require 'postgres_msf'
require 'postgres/buffer'
# TODO: Revisit this monkeypatch.
class IO
def read_exactly_n_bytes(n)
buf = read(n)
raise EOFError if buf == nil
return buf if buf.size == n
n -= buf.size
while n > 0
str = read(n)
raise EOFError if str == nil
buf << str
n -= str.size
end
return buf
end
end
# Namespace for Metasploit branch.
module Msf
module Db
module PostgresPR
class ParseError < RuntimeError; end
class DumpError < RuntimeError; end
# Base class representing a PostgreSQL protocol message
class Message
# One character message-typecode to class map
MsgTypeMap = Hash.new { UnknownMessageType }
def self.register_message_type(type)
raise "duplicate message type registration" if MsgTypeMap.has_key?(type)
MsgTypeMap[type] = self
self.const_set(:MsgType, type)
class_eval "def message_type; MsgType end"
end
def self.read(stream, startup=false)
type = stream.read_exactly_n_bytes(1) unless startup
length = stream.read_exactly_n_bytes(4).unpack('N').first # FIXME: length should be signed, not unsigned
raise ParseError unless length >= 4
# initialize buffer
buffer = Buffer.of_size(startup ? length : 1+length)
buffer.write(type) unless startup
buffer.write_int32_network(length)
buffer.copy_from_stream(stream, length-4)
(startup ? StartupMessage : MsgTypeMap[type]).create(buffer)
end
def self.create(buffer)
obj = allocate
obj.parse(buffer)
obj
end
def self.dump(*args)
new(*args).dump
end
def dump(body_size=0)
buffer = Buffer.of_size(5 + body_size)
buffer.write(self.message_type)
buffer.write_int32_network(4 + body_size)
yield buffer if block_given?
raise DumpError unless buffer.at_end?
return buffer.content
end
def parse(buffer)
buffer.position = 5
yield buffer if block_given?
raise ParseError, buffer.inspect unless buffer.at_end?
end
def self.fields(*attribs)
names = attribs.map {|name, type| name.to_s}
arg_list = names.join(", ")
ivar_list = names.map {|name| "@" + name }.join(", ")
sym_list = names.map {|name| ":" + name }.join(", ")
class_eval %[
attr_accessor #{ sym_list }
def initialize(#{ arg_list })
#{ ivar_list } = #{ arg_list }
end
]
end
end
class UnknownMessageType < Message
def dump
raise
end
end
class Authentification < Message
register_message_type 'R'
AuthTypeMap = Hash.new { UnknownAuthType }
def self.create(buffer)
buffer.position = 5
authtype = buffer.read_int32_network
klass = AuthTypeMap[authtype]
obj = klass.allocate
obj.parse(buffer)
obj
end
def self.register_auth_type(type)
raise "duplicate auth type registration" if AuthTypeMap.has_key?(type)
AuthTypeMap[type] = self
self.const_set(:AuthType, type)
class_eval "def auth_type() AuthType end"
end
# the dump method of class Message
alias message__dump dump
def dump
super(4) do |buffer|
buffer.write_int32_network(self.auth_type)
end
end
def parse(buffer)
super do
auth_t = buffer.read_int32_network
raise ParseError unless auth_t == self.auth_type
yield if block_given?
end
end
end
class UnknownAuthType < Authentification
end
class AuthentificationOk < Authentification
register_auth_type 0
end
class AuthentificationKerberosV4 < Authentification
register_auth_type 1
end
class AuthentificationKerberosV5 < Authentification
register_auth_type 2
end
class AuthentificationClearTextPassword < Authentification
register_auth_type 3
end
module SaltedAuthentificationMixin
attr_accessor :salt
def initialize(salt)
@salt = salt
end
def dump
raise DumpError unless @salt.size == self.salt_size
message__dump(4 + self.salt_size) do |buffer|
buffer.write_int32_network(self.auth_type)
buffer.write(@salt)
end
end
def parse(buffer)
super do
@salt = buffer.read(self.salt_size)
end
end
end
class AuthentificationCryptPassword < Authentification
register_auth_type 4
include SaltedAuthentificationMixin
def salt_size; 2 end
end
class AuthentificationMD5Password < Authentification
register_auth_type 5
include SaltedAuthentificationMixin
def salt_size; 4 end
end
class AuthentificationSCMCredential < Authentification
register_auth_type 6
end
class PasswordMessage < Message
register_message_type 'p'
fields :password
def dump
super(@password.size + 1) do |buffer|
buffer.write_cstring(@password)
end
end
def parse(buffer)
super do
@password = buffer.read_cstring
end
end
end
class ParameterStatus < Message
register_message_type 'S'
fields :key, :value
def dump
super(@key.size + 1 + @value.size + 1) do |buffer|
buffer.write_cstring(@key)
buffer.write_cstring(@value)
end
end
def parse(buffer)
super do
@key = buffer.read_cstring
@value = buffer.read_cstring
end
end
end
class BackendKeyData < Message
register_message_type 'K'
fields :process_id, :secret_key
def dump
super(4 + 4) do |buffer|
buffer.write_int32_network(@process_id)
buffer.write_int32_network(@secret_key)
end
end
def parse(buffer)
super do
@process_id = buffer.read_int32_network
@secret_key = buffer.read_int32_network
end
end
end
class ReadyForQuery < Message
register_message_type 'Z'
fields :backend_transaction_status_indicator
def dump
super(1) do |buffer|
buffer.write_byte(@backend_transaction_status_indicator)
end
end
def parse(buffer)
super do
@backend_transaction_status_indicator = buffer.read_byte
end
end
end
class DataRow < Message
register_message_type 'D'
fields :columns
def dump
sz = @columns.inject(2) {|sum, col| sum + 4 + (col ? col.size : 0)}
super(sz) do |buffer|
buffer.write_int16_network(@columns.size)
@columns.each {|col|
buffer.write_int32_network(col ? col.size : -1)
buffer.write(col) if col
}
end
end
def parse(buffer)
super do
n_cols = buffer.read_int16_network
@columns = (1..n_cols).collect {
len = buffer.read_int32_network
if len == -1
nil
else
buffer.read(len)
end
}
end
end
end
class CommandComplete < Message
register_message_type 'C'
fields :cmd_tag
def dump
super(@cmd_tag.size + 1) do |buffer|
buffer.write_cstring(@cmd_tag)
end
end
def parse(buffer)
super do
@cmd_tag = buffer.read_cstring
end
end
end
class EmptyQueryResponse < Message
register_message_type 'I'
end
module NoticeErrorMixin
attr_accessor :field_type, :field_values
def initialize(field_type=0, field_values=[])
raise ArgumentError if field_type == 0 and not field_values.empty?
@field_type, @field_values = field_type, field_values
end
def dump
raise ArgumentError if @field_type == 0 and not @field_values.empty?
sz = 1
sz += @field_values.inject(1) {|sum, fld| sum + fld.size + 1} unless @field_type == 0
super(sz) do |buffer|
buffer.write_byte(@field_type)
break if @field_type == 0
@field_values.each {|fld| buffer.write_cstring(fld) }
buffer.write_byte(0)
end
end
def parse(buffer)
super do
@field_type = buffer.read_byte
break if @field_type == 0
@field_values = []
while buffer.position < buffer.size-1
@field_values << buffer.read_cstring
end
terminator = buffer.read_byte
raise ParseError unless terminator == 0
end
end
end
class NoticeResponse < Message
register_message_type 'N'
include NoticeErrorMixin
end
class ErrorResponse < Message
register_message_type 'E'
include NoticeErrorMixin
end
# TODO
class CopyInResponse < Message
register_message_type 'G'
end
# TODO
class CopyOutResponse < Message
register_message_type 'H'
end
class Parse < Message
register_message_type 'P'
fields :query, :stmt_name, :parameter_oids
def initialize(query, stmt_name="", parameter_oids=[])
@query, @stmt_name, @parameter_oids = query, stmt_name, parameter_oids
end
def dump
sz = @stmt_name.size + 1 + @query.size + 1 + 2 + (4 * @parameter_oids.size)
super(sz) do |buffer|
buffer.write_cstring(@stmt_name)
buffer.write_cstring(@query)
buffer.write_int16_network(@parameter_oids.size)
@parameter_oids.each {|oid| buffer.write_int32_network(oid) }
end
end
def parse(buffer)
super do
@stmt_name = buffer.read_cstring
@query = buffer.read_cstring
n_oids = buffer.read_int16_network
@parameter_oids = (1..n_oids).collect {
# TODO: zero means unspecified. map to nil?
buffer.read_int32_network
}
end
end
end
class ParseComplete < Message
register_message_type '1'
end
class Query < Message
register_message_type 'Q'
fields :query
def dump
super(@query.size + 1) do |buffer|
buffer.write_cstring(@query)
end
end
def parse(buffer)
super do
@query = buffer.read_cstring
end
end
end
class RowDescription < Message
register_message_type 'T'
fields :fields
class FieldInfo < Struct.new(:name, :oid, :attr_nr, :type_oid, :typlen, :atttypmod, :formatcode); end
def dump
sz = @fields.inject(2) {|sum, fld| sum + 18 + fld.name.size + 1 }
super(sz) do |buffer|
buffer.write_int16_network(@fields.size)
@fields.each { |f|
buffer.write_cstring(f.name)
buffer.write_int32_network(f.oid)
buffer.write_int16_network(f.attr_nr)
buffer.write_int32_network(f.type_oid)
buffer.write_int16_network(f.typlen)
buffer.write_int32_network(f.atttypmod)
buffer.write_int16_network(f.formatcode)
}
end
end
def parse(buffer)
super do
n_fields = buffer.read_int16_network
@fields = (1..n_fields).collect {
f = FieldInfo.new
f.name = buffer.read_cstring
f.oid = buffer.read_int32_network
f.attr_nr = buffer.read_int16_network
f.type_oid = buffer.read_int32_network
f.typlen = buffer.read_int16_network
f.atttypmod = buffer.read_int32_network
f.formatcode = buffer.read_int16_network
f
}
end
end
end
class StartupMessage < Message
fields :proto_version, :params
def dump
sz = @params.inject(4 + 4) {|sum, kv| sum + kv[0].size + 1 + kv[1].size + 1} + 1
buffer = Buffer.of_size(sz)
buffer.write_int32_network(sz)
buffer.write_int32_network(@proto_version)
@params.each_pair {|key, value|
buffer.write_cstring(key)
buffer.write_cstring(value)
}
buffer.write_byte(0)
raise DumpError unless buffer.at_end?
return buffer.content
end
def parse(buffer)
buffer.position = 4
@proto_version = buffer.read_int32_network
@params = {}
while buffer.position < buffer.size-1
key = buffer.read_cstring
val = buffer.read_cstring
@params[key] = val
end
nul = buffer.read_byte
raise ParseError unless nul == 0
raise ParseError unless buffer.at_end?
end
end
class SSLRequest < Message
fields :ssl_request_code
def dump
sz = 4 + 4
buffer = Buffer.of_size(sz)
buffer.write_int32_network(sz)
buffer.write_int32_network(@ssl_request_code)
raise DumpError unless buffer.at_end?
return buffer.content
end
def parse(buffer)
buffer.position = 4
@ssl_request_code = buffer.read_int32_network
raise ParseError unless buffer.at_end?
end
end
=begin
# TODO: duplicate message-type, split into client/server messages
class Sync < Message
register_message_type 'S'
end
=end
class Terminate < Message
register_message_type 'X'
end
end # module PostgresPR
end
end

View File

@ -0,0 +1,161 @@
# This is a compatibility layer for using the pure Ruby postgres-pr instead of
# the C interface of postgres.
require 'postgres_msf'
require 'postgres/postgres-pr/connection'
# Namespace for Metasploit branch.
module Msf
module Db
class PGconn
class << self
alias connect new
end
def initialize(host, port, options, tty, database, user, auth)
uri =
if host.nil?
nil
elsif host[0] != ?/
"tcp://#{ host }:#{ port }"
else
"unix:#{ host }/.s.PGSQL.#{ port }"
end
@host = host
@db = database
@user = user
@conn = PostgresPR::Connection.new(database, user, auth, uri)
end
def close
@conn.close
end
attr_reader :host, :db, :user
def query(sql)
PGresult.new(@conn.query(sql))
end
alias exec query
def transaction_status
@conn.transaction_status
end
def self.escape(str)
str.gsub("'","''").gsub("\\", "\\\\\\\\")
end
def notice_processor
@conn.notice_processor
end
def notice_processor=(np)
@conn.notice_processor = np
end
def self.quote_ident(name)
%("#{name}")
end
end
class PGresult
include Enumerable
EMPTY_QUERY = 0
COMMAND_OK = 1
TUPLES_OK = 2
COPY_OUT = 3
COPY_IN = 4
BAD_RESPONSE = 5
NONFATAL_ERROR = 6
FATAL_ERROR = 7
def each(&block)
@result.each(&block)
end
def [](index)
@result[index]
end
def initialize(res)
@res = res
@fields = @res.fields.map {|f| f.name}
@result = @res.rows
end
# TODO: status, getlength, cmdstatus
attr_reader :result, :fields
def num_tuples
@result.size
end
def num_fields
@fields.size
end
def fieldname(index)
@fields[index]
end
def fieldnum(name)
@fields.index(name)
end
def type(index)
# TODO: correct?
@res.fields[index].type_oid
end
def size(index)
raise
# TODO: correct?
@res.fields[index].typlen
end
def getvalue(tup_num, field_num)
@result[tup_num][field_num]
end
def status
if num_tuples > 0
TUPLES_OK
else
COMMAND_OK
end
end
def cmdstatus
@res.cmd_tag || ''
end
# free the result set
def clear
@res = @fields = @result = nil
end
# Returns the number of rows affected by the SQL command
def cmdtuples
case @res.cmd_tag
when nil
return nil
when /^INSERT\s+(\d+)\s+(\d+)$/, /^(DELETE|UPDATE|MOVE|FETCH)\s+(\d+)$/
$2.to_i
else
nil
end
end
end
class PGError < Exception
end
end
end

View File

@ -0,0 +1,46 @@
require 'strscan'
module Postgres::Conversion
def decode_array(str, delim=',', &conv_proc)
delim = Regexp.escape(delim)
buf = StringScanner.new(str)
return parse_arr(buf, delim, &conv_proc)
ensure
raise ConversionError, "end of string expected (#{buf.rest})" unless buf.empty?
end
private
def parse_arr(buf, delim, &conv_proc)
# skip whitespace
buf.skip(/\s*/)
raise ConversionError, "'{' expected" unless buf.get_byte == '{'
elems = []
unless buf.scan(/\}/) # array is not empty
loop do
# skip whitespace
buf.skip(/\s+/)
elems <<
if buf.check(/\{/)
parse_arr(buf, delim, &conv_proc)
else
e = buf.scan(/("((\\.)|[^"])*"|\\.|[^\}#{ delim }])*/) || raise(ConversionError)
if conv_proc then conv_proc.call(e) else e end
end
break if buf.scan(/\}/)
break unless buf.scan(/#{ delim }/)
end
end
# skip whitespace
buf.skip(/\s*/)
elems
end
end

View File

@ -0,0 +1,26 @@
module Postgres::Conversion
#
# Encodes a string as bytea value.
#
# for encoding rules see:
# http://www.postgresql.org/docs/7.4/static/datatype-binary.html
#
def encode_bytea(str)
str.gsub(/[\000-\037\047\134\177-\377]/) {|b| "\\#{ b[0].to_s(8).rjust(3, '0') }" }
end
#
# Decodes a bytea encoded string.
#
# for decoding rules see:
# http://www.postgresql.org/docs/7.4/static/datatype-binary.html
#
def decode_bytea(str)
str.gsub(/\\(\\|'|[0-3][0-7][0-7])/) {|s|
if s.size == 2 then s[1,1] else s[1,3].oct.chr end
}
end
end

View File

@ -0,0 +1,5 @@
module Postgres
module Conversion
class ConversionError < Exception; end
end
end

View File

@ -0,0 +1,10 @@
# Namespace for Metasploit branch.
module Msf
module Db
module PostgresPR
Version = "0.6.3-msf"
end
end
end

12
lib/postgres_msf.rb Normal file
View File

@ -0,0 +1,12 @@
# "Pure Ruby PostgreSQL interface," also known as "Postgres-PR" is:
# Copyright (c) 2005, 2008 by Michael Neumann (mneumann@ntecs.de).
#
# Postgres-PR is released under the same terms of license as Ruby.
#
# The Ruby License is:
# http://www.ruby-lang.org/en/LICENSE.txt
#
require 'postgres/postgres-pr/postgres-compat'
require 'stringio'

53
lib/postgres_msf.rb.ut.rb Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env ruby
require 'test/unit'
require 'postgres_msf'
$_POSTGRESQL_TEST_SERVERNAME = 'dbsrv' # Name or IP, default: dbsrv
$_POSTGRESQL_TEST_SERVERPORT = 5432 # Default: 5432
$_POSTGRESQL_TEST_DATABASE = 'mydb' # Default: mydb
$_POSTGRESQL_TEST_USERNAME = 'scott' # Default: scott
$_POSTGRESQL_TEST_PASSWORD = 'tiger' # Default: tiger
class Msf::Db::PostgresPR::UnitTest < ::Test::Unit::TestCase
def test_connection
srv = "tcp://#{$_POSTGRESQL_TEST_SERVERNAME}:#{$_POSTGRESQL_TEST_SERVERPORT}"
conn = Msf::Db::PostgresPR::Connection.new($_POSTGRESQL_TEST_DATABASE,
$_POSTGRESQL_TEST_USERNAME,
$_POSTGRESQL_TEST_PASSWORD,
srv)
assert_kind_of Msf::Db::PostgresPR::Connection, conn
assert_nothing_raised { conn.close }
end
# Note that this will drop the "test" table for the named database.
# This is a destructive act!
def test_query
srv = "tcp://#{$_POSTGRESQL_TEST_SERVERNAME}:#{$_POSTGRESQL_TEST_SERVERPORT}"
conn = Msf::Db::PostgresPR::Connection.new($_POSTGRESQL_TEST_DATABASE,
$_POSTGRESQL_TEST_USERNAME,
$_POSTGRESQL_TEST_PASSWORD,
srv)
begin
conn.query("drop table test")
rescue RuntimeError # Cleanup, it may or may not be there.
end
assert_nothing_raised do
conn.query("CREATE TABLE test (i int, v varchar(5))")
conn.query(%q{INSERT INTO test VALUES (1, 'foo')})
conn.query(%q{INSERT INTO test VALUES (2, 'bar')})
end
resp = conn.query("select * from test")
assert_equal(2, resp.rows.size)
assert_equal(2, resp.fields.size)
assert_equal("SELECT", resp.cmd_tag)
assert_nothing_raised { conn.query("drop table test") }
end
end