续上一篇: 从零写一个兼容MySQL/Oracle的Proxy中件间(一)《初识Oracle的通信协议》
0.前言
昨天的文字里写开发这个中间件的原由和要解决的问题,有朋友留言
网上有现成的开源中间件为啥不用。
答:网上有很多MySQL的中件间,Oralce目前还没有可以免费使用的中件间. 这可能就是开源和闭源的差别。
Oracle自带的功能已经可以实现想要的功能(高可用/审计日志)
答:
- Oracle官方的高可用方案RAC,无疑是非常非常非常优秀的,但我们现有的硬件不支持做跨机房RAC,以及我们迁移时需要proxy中间层来降低业务中断时间。
- Oracle的审计日志太笨重/不支持慢日志/不支持SQL黑名单。
1.昨天我们实现了以下功能]
- 捕获了Oracle通信协议中的用户登录包
- 抓到了用户传用户名和密码的内容(密码是加密串)
- 同时通过对比,确定了用户发送SQL请求的通信包
- SQL日志:分析这些包,把SQL语句拿出来,记到日志里。
- SQL改写:用户发起的SQL 经过中间层改写到了服务端收到的是另一个SQL执行返回结果。
开始动手:
步骤一:从Oracle通信包中分解出SQL语句
已知有以下两种head的包是在传递SQL
0x1 0xf 0x0 0x0 0x6 0x0 0x0 0x0 0x0 0x0 0x11 0x6b 0x4 0xa5 0x10 0x0 0x0 0x35 0x1c 0x0 0x0 0x1 0x0 0x0 0x0 0x3 0x5e 0x5 0x61 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0xfe 0xff 0xff 0xff
0x1 0x0 0x0 0x0 0x6 0x0 0x0 0x0 0x0 0x0 0x3 0x5e 0x6 0x61 0x80 0x0 0x0 0x0 0x0 0x0 0x0 0xfe 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x24 0x0 0x0 0x0 0xfe 0xff 0xff 0xff 0xff 0xff 0xff
1.写一个Python脚本,用来连接并执行两个SQL
#!/usr/bin/env python
## coding: utf-8
import cx_Oracle
conn = cx_Oracle.connect('dboopreader/dbooppassword@127.0.0.1:1106/tlionrdb')
print("连接成功")
curs = conn.cursor()
sql = 'select 1 from dual'
curs.execute(sql)
for result in curs:
print(f"执行sql[ select 1 from dual ]返回{str(result)}")
sql = 'select 5 from dual'
curs.execute(sql)
for result in curs:
print(f"执行sql[ select 5 from dual ]返回{str(result)}")
curs.close()
conn.close()
print("连接关闭")
2.在本地启用一个1106端口的服务,拦截到Oracle请求。并在找到特定两种head头的包,解析其中的SQL
代码如下:
if eqByte(bLogininfo[0:], buffer[0:20]) {
log.Printf("抓到:%s-->%s的用户登录包\n", sqlInfo.client, sqlInfo.server)
log.Printf("%s\n", getLogininfo(buffer[58:260]))
} else if eqByte(sqlfinfo[3:11], buffer[3:11]) || eqByte(sql2info[3:11], buffer[3:11]) {
log.Printf("抓到:%s-->%s的用户SQL包\n", sqlInfo.client, sqlInfo.server)
sqlstr := getSQLinfo(buffer[180:])
log.Printf("SQL:%s\n", sqlstr)
} else {
log.Printf("抓到:%s到%s包\n", sqlInfo.client, sqlInfo.server)
//printBufferHead(buffer, 40)
}
func getSQLinfo(buffer []byte) string {
var bufferNew bytes.Buffer
for _, v := range buffer {
if v < 0x20 {
continue
} else if v == 0x80 {
break
} else if v > 0 {
bufferNew.WriteString(string(v))
}
}
return bufferNew.String()
}
3.执行Python脚本
这里有个不太严谨的地方,用0x80
当成SQL终止的标识,可能不一定准,但我目前分析的包都没问题,
一个简单的SQL日志功能就完成了
步骤2:SQL改写
既然可以记录用户请求的SQL,是不是可以改写这个包,以实现拦截部分指定SQL,改写指定的表(例如把 user表的请求,改写成user01)
这块有点复杂,先试一个简单的
把用户的请求:select 1 from dual;
改写成 :select 2 from dual;
这样用户select 1,结果返回了2,是不是很崩溃??图片
续着上一段代码,增加了改写SQL部分
if eqByte(bLogininfo[0:], buffer[0:20]) {
log.Printf("抓到:%s-->%s的用户登录包\n", sqlInfo.client, sqlInfo.server)
log.Printf("%s\n", getLogininfo(buffer[58:260]))
} else if eqByte(sqlfinfo[3:11], buffer[3:11]) || eqByte(sql2info[3:11], buffer[3:11]) {
log.Printf("抓到:%s-->%s的用户SQL包\n", sqlInfo.client, sqlInfo.server)
sqlstr := getSQLinfo(buffer[180:])
log.Printf("SQL:%s\n", sqlstr)
if strings.Contains(sqlstr, "select 1") {
//如果发现sql里有select 1 将它改写成 select 2
bIndex, eIndex := getSQLIndex(buffer[0:])
for j := bIndex; j < eIndex; j++ {
if buffer[j] == 0x31 {
log.Printf("这里%d用户发起的select 1,替换成了 select 2", j)
buffer[j] = 0x32 //这里把用户发起的select 1,替换成了 select 2
break
}
}
}
} else {
log.Printf("抓到:%s到%s包\n", sqlInfo.client, sqlInfo.server)
//printBufferHead(buffer, 40)
}
这样我们就通过在中间层拦截用户的SQL,并改写了它。
今天就写到这了,还有些其他的工作要处理,明天继续。
>> Home