diff --git a/lib/postgres/binary_reader.rb b/lib/postgres/binary_reader.rb new file mode 100644 index 0000000000..a5c7ec7449 --- /dev/null +++ b/lib/postgres/binary_reader.rb @@ -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 diff --git a/lib/postgres/binary_writer.rb b/lib/postgres/binary_writer.rb new file mode 100644 index 0000000000..e8d2579452 --- /dev/null +++ b/lib/postgres/binary_writer.rb @@ -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 diff --git a/lib/postgres/buffer.rb b/lib/postgres/buffer.rb new file mode 100644 index 0000000000..1c5f988fd6 --- /dev/null +++ b/lib/postgres/buffer.rb @@ -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 diff --git a/lib/postgres/byteorder.rb b/lib/postgres/byteorder.rb new file mode 100644 index 0000000000..f12c54dacc --- /dev/null +++ b/lib/postgres/byteorder.rb @@ -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 diff --git a/lib/postgres/postgres-pr/connection.rb b/lib/postgres/postgres-pr/connection.rb new file mode 100644 index 0000000000..5a8350ef83 --- /dev/null +++ b/lib/postgres/postgres-pr/connection.rb @@ -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 diff --git a/lib/postgres/postgres-pr/message.rb b/lib/postgres/postgres-pr/message.rb new file mode 100644 index 0000000000..568891b786 --- /dev/null +++ b/lib/postgres/postgres-pr/message.rb @@ -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 diff --git a/lib/postgres/postgres-pr/postgres-compat.rb b/lib/postgres/postgres-pr/postgres-compat.rb new file mode 100644 index 0000000000..4067eb19cb --- /dev/null +++ b/lib/postgres/postgres-pr/postgres-compat.rb @@ -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 diff --git a/lib/postgres/postgres-pr/typeconv/array.rb b/lib/postgres/postgres-pr/typeconv/array.rb new file mode 100644 index 0000000000..235d545220 --- /dev/null +++ b/lib/postgres/postgres-pr/typeconv/array.rb @@ -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 diff --git a/lib/postgres/postgres-pr/typeconv/bytea.rb b/lib/postgres/postgres-pr/typeconv/bytea.rb new file mode 100644 index 0000000000..7c43ac9e54 --- /dev/null +++ b/lib/postgres/postgres-pr/typeconv/bytea.rb @@ -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 diff --git a/lib/postgres/postgres-pr/typeconv/conv.rb b/lib/postgres/postgres-pr/typeconv/conv.rb new file mode 100644 index 0000000000..c2f9ee0899 --- /dev/null +++ b/lib/postgres/postgres-pr/typeconv/conv.rb @@ -0,0 +1,5 @@ +module Postgres + module Conversion + class ConversionError < Exception; end + end +end diff --git a/lib/postgres/postgres-pr/version.rb b/lib/postgres/postgres-pr/version.rb new file mode 100644 index 0000000000..821fb8f375 --- /dev/null +++ b/lib/postgres/postgres-pr/version.rb @@ -0,0 +1,10 @@ +# Namespace for Metasploit branch. +module Msf +module Db + +module PostgresPR + Version = "0.6.3-msf" +end + +end +end diff --git a/lib/postgres_msf.rb b/lib/postgres_msf.rb new file mode 100644 index 0000000000..8a89767a64 --- /dev/null +++ b/lib/postgres_msf.rb @@ -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' + diff --git a/lib/postgres_msf.rb.ut.rb b/lib/postgres_msf.rb.ut.rb new file mode 100644 index 0000000000..b6bdd6c6f1 --- /dev/null +++ b/lib/postgres_msf.rb.ut.rb @@ -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 +