当前版本在my_webroot目录中增加了v0_22_0的子目录,该目录中存放了一些测试脚本,通过这些脚本可以实现支付宝的支付测试。

    页面导航:

项目下载地址:

    zenglServer源代码的相关地址:https://github.com/zenglong/zenglServer  当前版本对应的tag标签为:v0.22.0

zenglServer v0.22.0:

    当前版本在my_webroot目录中增加了v0_22_0的子目录,该目录中存放了一些测试脚本,通过这些脚本可以实现支付宝的支付测试。

    要进行支付测试,首先需要配置v0_22_0/config.zl脚本,该脚本用于配置和支付宝相关的APPID,商户密钥等信息:

// 支付宝的APPID(发起请求的应用ID)
config['app_id'] = '';
// 支付完成后的异步通知地址,必须是外网可以访问到的地址
config['notify_url'] = 'http://domain_url/v0_22_0/notify_url.zl';
// 支付完成后跳转返回到商家的地址
config['return_url'] = 'http://domain_url/v0_22_0/return_url.zl';
// 签名方式,目前测试脚本只支持最新的RSA2方式,所以不需要修改
config['sign_type'] = 'RSA2';
// 支付宝网关,沙箱的网关地址和正式环境的网关地址不同
config['gateway_url'] = 'https://openapi.alipaydev.com/gateway.do';
// 商户私钥
config['merchant_private_key'] = '';
// 支付宝公钥
config['alipay_public_key'] = '';

    支付宝APPID之类的信息可以在支付宝的开放平台中获取到。

    在配置完上面这些支付宝相关的信息后,就可以进行相关的支付测试了。在my_webroot/v0_22_0子目录中,包含了test.html等静态页面,这些静态页面是完成支付测试,交易查询,退款,退款查询以及交易关闭操作的入口页面。

    要进行支付测试,先在浏览器中打开test.html页面,如下所示:

test.html支付测试入口页面

    点击付款按钮,可以先看到付款相关的调试输出信息:

付款相关的调试输出页面

    点击ok按钮,就可以跳转到支付宝的支付页面:

支付宝的支付页面

    在该页面可以使用扫码支付方式,也可以使用账号密码的支付方式。支付成功后,就会跳转回v0_22_0/return_url.zl的商家页面:

支付宝支付成功后,跳转返回到商家页面

    要测试支付查询的话,可以使用test_query.html的入口页面:

交易查询入口页面

    在交易查询页面,输入订单号,点击交易查询按钮,就可以看到支付宝返回的查询结果:

交易查询结果

    要测试退款功能,可以使用test_refund.html页面。要测试退款查询,可以使用test_refund_query.html页面。要测试交易关闭,可以使用test_close.html页面,这些页面的输出结果都类似上面的交易查询结果,只是支付宝返回的数据不同而已。

test.zl脚本:

    上面的支付宝付款,交易查询,退款等入口页面,最终都会将请求提交给v0_22_0目录中的test.zl脚本,去完成具体的业务逻辑处理,该脚本的代码如下:

use builtin,request,openssl,curl,pcre;

// 通过本脚本实现支付宝的电脑端付款,交易查询,退款,退款查询以及交易关闭的后端处理

inc 'config.zl';
inc 'func.zl';

gl_use_html = TRUE;

print '<!Doctype html>';
print '<html><head><meta http-equiv="content-type" content="text/html;charset=utf-8" /></head><body>';

if(bltIsRunInCmd())
	exit('must be run in website');
else
	is_cmd = FALSE;
	br = '<br/>';
endif

body_array = rqtGetBodyAsArray();

// 如果选择了立即付款,就设置immediate_pay变量为TRUE,这样在付款时,就不会输出调试信息,而是会直接跳转到支付宝的支付页面
immediate_pay = FALSE;
if(bltStr(&body_array['sb']) == '测试立即付款')
	immediate_pay = TRUE;
endif

for(i=0;bltIterArray(body_array,&i,&k,&v);)
	print_msg(k +": " + v + br);
endfor

action = bltStr(&body_array['action']);
// 除了支付宝付款操作是跳转到支付宝的支付页面外,其他的像交易查询,退款之类的操作,则都会使用curl库直接请求支付宝的网关来完成相关的操作
use_curl = TRUE;
/**
 * 根据前端页面提交的action来判断需要执行什么操作,例如,当action为query时表示需要执行的是交易查询操作等
 * 然后根据需要执行的操作,来设置biz_content_arr数组的成员值(该数组中包含了前端提交过来的out_trade_no商户订单号,total_amount付款金额等信息)
 * 以及设置method变量,该变量存储了需要传递给支付宝网关的方法名,例如,交易查询操作时,method方法名会是 alipay.trade.query
 */
// action等于query表示执行交易查询操作
if(action == 'query')
	biz_content_arr['out_trade_no'] = body_array['out_trade_no'];
	biz_content_arr['trade_no'] = body_array['trade_no'];
	method = 'alipay.trade.query';
// action等于refund表示执行退款操作
elif(action == 'refund')
	biz_content_arr['out_trade_no'] = body_array['out_trade_no'];
	biz_content_arr['trade_no'] = body_array['trade_no'];
	biz_content_arr['refund_amount'] = body_array['refund_amount'];
	biz_content_arr['refund_reason'] = body_array['refund_reason'];
	biz_content_arr['out_request_no'] = body_array['out_request_no'];
	method = 'alipay.trade.refund';
// action等于refund_query表示执行退款查询操作
elif(action == 'refund_query')
	biz_content_arr['out_trade_no'] = body_array['out_trade_no'];
	biz_content_arr['trade_no'] = body_array['trade_no'];
	biz_content_arr['out_request_no'] = body_array['out_request_no'];
	method = 'alipay.trade.fastpay.refund.query';
// action等于close表示执行交易关闭操作
elif(action == 'close')
	biz_content_arr['out_trade_no'] = body_array['out_trade_no'];
	biz_content_arr['trade_no'] = body_array['trade_no'];
	method = 'alipay.trade.close';
// 没传action,则表示执行付款操作
else
	biz_content_arr['product_code'] = 'FAST_INSTANT_TRADE_PAY';
	biz_content_arr['body'] = body_array['body'];
	biz_content_arr['subject'] = body_array['subject'];
	biz_content_arr['total_amount'] = body_array['total_amount'];
	biz_content_arr['out_trade_no'] = body_array['out_trade_no'];
	method = 'alipay.trade.page.pay';
	use_curl = FALSE;
endif

// 将biz_content_arr转为json字符串,该json字符串会作为请求参数传递给支付宝网关
biz_content = bltJsonEncode(biz_content_arr);
print_msg('biz_content:' + biz_content + br + br);

// 设置params数组,该数组里的成员值会转为http请求参数传递给支付宝网关
params['biz_content'] = biz_content;
params['method'] = method;
params['alipay_sdk'] = 'alipay-sdk-zengl-20200627';
params['charset'] = 'UTF-8';
params['format'] = 'json';
params['version'] = '1.0';
params['timestamp'] = bltDate('%Y-%m-%d %H:%M:%S');
params['app_id'] = config['app_id'];
params['sign_type'] = config['sign_type'];
params['notify_url'] = config['notify_url'];
params['return_url'] = config['return_url'];

// 通过sort_array脚本函数(在func.zl脚本中定义),将params请求参数数组,按照key(键名)的ASCII码序从小到大进行排序
sort_params =  sort_array(params);

// 通过get_sign_data脚本函数(也定义在func.zl脚本中),将sort_params排序过的请求参数数组,
// 转为需要签名的字符串,数组成员之间通过&符号连接,每个成员的key(键名)和对应的值之间用=号连接
str_to_be_signed = get_sign_data(sort_params);

// 在非立即付款操作下,将需要签名的字符串信息打印出来
print_msg('str_to_be_signed:' + bltHtmlEscape(str_to_be_signed) + br + br);

// 通过add_key_header_footer脚本函数(也定义在func.zl脚本中),将支付宝的商户私钥转为openssl密钥格式
key_content = add_key_header_footer(config['merchant_private_key'], 64, '-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----');
// 根据openssl格式的商户私钥,读取该私钥,并返回相应的密钥key指针,该key会用于下面的签名操作
key = opensslReadKey(key_content, RSA_PRIVATE);
if(key == NULL)
	exit('read key failed: ' + opensslGetError());
endif

// 使用SHA256的RSA签名类型(也就是支付宝所要求的RSA2的签名方式),以及使用EVP_为前缀的底层库函数进行签名操作,得到的签名二进制数据存放在sign变量中
ret = opensslSign(str_to_be_signed, -1, key, &sign, &sign_len, RSA_SIGN_SHA256, USE_EVP);
if(!ret)
	exit('sign failed: ' + opensslGetError());
endif

// 将签名二进制数据转为base64编码格式,这样签名数据就能以字符串的形式通过http请求传递给支付宝网关
sign_encode = bltBase64Encode(sign);

// 在非立即付款操作下,将base64编码的签名打印出来
print_msg('sign_encode:' + sign_encode + br + br);

// 将base64编码的签名设置到sort_params请求参数数组中
sort_params['sign'] = sign_encode;

// 如果是交易查询,退款,退款查询以及交易关闭的操作,则会通过curl直接请求支付宝网关来完成相关的操作
if(use_curl)
	curl_handle = curlEasyInit();
	// 根据config配置(定义在config.zl脚本中)里的gateway_url支付宝网关地址,来设置curl的目标url地址
	curlEasySetopt(curl_handle, 'URL', config['gateway_url'] + "?charset=" + sort_params['charset']);
	curlEasySetopt(curl_handle, 'FOLLOWLOCATION', TRUE);
	// 不校验SSL证书
	curlEasySetopt(curl_handle, 'SSL_VERIFYPEER', FALSE);
	// 不校验域名与证书中的CN(common name)字段是不是匹配
	curlEasySetopt(curl_handle, 'SSL_VERIFYHOST', FALSE);
	curlEasySetopt(curl_handle, 'TIMEOUT', 30);
	submit_post = '';
	// 将sort_params请求参数数组里的成员经过url编码后,转为&符号连接的url格式
	for(i=0;bltIterArray(sort_params,&i,&k,&v);)
		submit_post += k + '=' + bltUrlEncode(v);
		if(i < bltCount(sort_params))
			submit_post += '&';
		endif
	endfor
	// 将上面得到的url格式的请求参数,设置到curl的POSTFIELDS选项,以作为curl的post请求参数
	curlEasySetopt(curl_handle, 'POSTFIELDS', submit_post);
	// 通过curl向支付宝网关发送post请求
	ret = curlEasyPerform(curl_handle, &content);
	// 如果ret返回值为0,就说明curl执行成功,则将支付宝网关返回的数据显示出来,同时对支付宝网关的返回数据进行验签操作,判断数据是不是没有被中间节点修改过
	if(ret == 0)
		print 'curl response content: ' + content + br;
		con_decode = bltJsonDecode(content);
		method_response = bltStrReplace(method, '.', '_') + '_response';
		print 'method_response: ' + method_response + br + br;
		response_sign = con_decode['sign'];
		print 'response_sign: ' + response_sign + br + br;
		match_ret = pcreMatch('^{"'+method_response+'":(.*?),"sign":.*?$', content, &results);
		if(match_ret)
			response_data = results[1];
		else
			response_data = '';
		endif
		print 'response_data: ' + (response_data != '' ? response_data : '没有匹配到需要验签的数据') + br + br;
		key_pub_content = add_key_header_footer(config['alipay_public_key'], 64, '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----');
		ret = check_sign(key_pub_content, response_data, response_sign);
		if(ret)
			print '验签成功,格式化数据如下:' + br;
			for(j=0;bltIterArray(con_decode[method_response],&j,&inner_k,&inner_v);)
				print ' -- ' + inner_k +": " + inner_v + br;
			endfor
		else
			print '验签失败';
		endif
	else
		print 'curl error: ' + curlEasyStrError(ret);
	endif
	curlEasyCleanup(curl_handle);
// 如果是支付宝付款操作,则直接跳转到支付宝的支付页面
else
	if(immediate_pay)
		hidden_style = " style = 'display:none;'";
	else
		hidden_style = '';
	endif
	submit_html = "<div"+hidden_style+"><form id='alipaysubmit' name='alipaysubmit' action='" + 
			config['gateway_url'] + "?charset=" + sort_params['charset'] + "' method='POST'>\n";
	for(i=0;bltIterArray(sort_params,&i,&k,&v);)
		v = bltStrReplace(v, "'", "&apos;");
		submit_html += "<div><input name='" + k + "' value='" + v + "'/></div>\n";
	endfor
	submit_html += "<div><input type='submit' value='ok'></div>\n</form></div>";
	if(immediate_pay)
		submit_html += "<div>正在跳转到支付宝支付页面...</div><script>document.forms['alipaysubmit'].submit();</script>";
	endif
	print submit_html;
endif

print '</body></html>';

notify_url.zl脚本:

    在v0_22_0目录中的notify_url.zl脚本可以接收支付成功时,支付宝发送给服务端的异步通知,可以在异步通知中知道支付订单是否执行成功了等,该脚本的代码如下:

use builtin,request,openssl,curl,pcre;

inc 'config.zl';
inc 'func.zl';

// 获取支付宝异步通知的post请求数组
body_array = rqtGetBodyAsArray();

// 通过sort_array脚本函数(在func.zl脚本中定义),将body_array请求数组的成员,按照key(键名)的ASCII码序从小到大进行排序
sort_body_arr = sort_array(body_array);

// post请求参数中的sign是支付宝生成的base64格式的签名
sign = sort_body_arr['sign'];
// 将请求数组中的sign和sign_type成员去掉,因为支付宝异步通知时生成的sign签名,是没有包含sign和sign_type字段的,因此,验签时也要去除这两个字段
bltUnset(&sort_body_arr['sign'], &sort_body_arr['sign_type']);

// 通过get_sign_data脚本函数(也定义在func.zl脚本中),将sort_body_arr排序过的post请求参数数组,
// 转为需要进行验签的字符串,数组成员之间通过&符号连接,每个成员的key(键名)和对应的值之间用=号连接
data = get_sign_data(sort_body_arr);

// 通过add_key_header_footer脚本函数(也定义在func.zl脚本中),将支付宝公钥转为openssl密钥格式
key_content = add_key_header_footer(config['alipay_public_key'], 64, '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----');
// 通过check_sign脚本函数(也定义在func.zl脚本中),使用支付宝公钥进行验签
ret = check_sign(key_content, data, sign);

if(ret)
	// 验签成功,则返回success
	retval = 'success';
else
	// 验签失败,则返回fail
	retval = 'fail';
endif

// 将签名之类的信息写入notify_url.log日志文件中,方便调试开发
bltWriteFile('notify_url.log', bltDate('%Y-%m-%d %H:%M:%S') + '\nbody: ' + bltUrlDecode(rqtGetBody())
	 + '\n\ndata: ' + data + '\n\nsign: ' + sign + '\n\nretval: ' + retval);

// 通过bltOutputBlob模块函数输出success或fail,和print指令相比,该模块函数不会在末尾添加换行符,只会原样输出字符串信息
bltOutputBlob(retval, -1);

    上面脚本在验签成功时,会返回success给支付宝,否则返回fail,这些返回信息是支付宝约定的返回信息,用于告诉支付宝异步通知是否执行成功了的,请勿将success改成其他的字符串。

return_url.zl脚本:

    之前提到过,在执行付款操作时,如果支付成功,支付宝会跳转到商家的返回页面。在当前版本中,就是跳转到v0_22_0/return_url.zl脚本,该脚本的代码如下:

use builtin,request,openssl,curl,pcre;

inc 'config.zl';
inc 'func.zl';

// 获取支付宝返回到商家页面时传递的http请求参数数组
query_array = rqtGetQuery();

// 通过sort_array脚本函数(在func.zl脚本中定义),将query_array请求参数数组的成员,按照key(键名)的ASCII码序从小到大进行排序
sort_query_array = sort_array(query_array);

// 请求参数中的sign是支付宝生成的base64格式的签名
sign = sort_query_array['sign'];

// 支付宝返回到商家时生成的sign签名,是没有包含sign字段的,因此,验签时也要去除这个字段
bltUnset(&sort_query_array['sign']);
data_no_sign = get_sign_data(sort_query_array);

// 支付宝返回到商家时生成的sign签名,还有可能是没有包含sign_type字段的,因此,可以对去除了sign和sign_type的签名,以及上面只去除了sign的签名进行双重验证,只要有一个验证通过就行
bltUnset(&sort_query_array['sign_type']);
data_no_sign_and_type = get_sign_data(sort_query_array);

// 通过add_key_header_footer脚本函数(也定义在func.zl脚本中),将支付宝公钥转为openssl密钥格式
key_content = add_key_header_footer(config['alipay_public_key'], 64, '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----');
// 通过check_sign脚本函数(也定义在func.zl脚本中),使用支付宝公钥进行验签
ret = check_sign(key_content, data_no_sign_and_type, sign);
check_type = '(去除了sign和sign_type的签名验证)';
if(!ret)
	ret = check_sign(key_content, data_no_sign, sign);
	check_type = '(只去除了sign的签名验证)';
endif

print '<!Doctype html>';
print '<html><head><meta http-equiv="content-type" content="text/html;charset=utf-8" /></head><body>';

if(ret)
	retval = '验证成功! ' + check_type + '<br />支付宝交易号:' + sort_query_array['trade_no'];
else
	retval = '验证失败';
endif

// 将支付宝传递过来的相关信息写入return_url.log日志文件中,方便调试开发
bltWriteFile('return_url.log', bltDate('%Y-%m-%d %H:%M:%S') + '\nquery string:' + bltUrlDecode(rqtGetQueryAsString()) 
		+ '\n\ndata_no_sign_and_type: ' + data_no_sign_and_type
		+ '\n\ndata_no_sign: ' + data_no_sign
		+ '\n\nsign: ' + sign + '\n\nretval: ' + retval);

// 将验证成功,或验证失败的信息反馈给客户端
print retval + '\n</body></html>';

    为了兼容旧的签名验证,上面脚本中采用了双重验证方式,即当旧的去除了sign和sign_type的签名验证失败时,就再尝试新的只去除了sign的签名验证。

bltUrlEncode模块函数:

    当前版本增加了bltUrlEncode模块函数,可以对字符串进行url编码,该模块函数定义在module_builtin.c文件中:

/**
 * bltUrlEncode模块函数,对字符串参数进行url编码
 * 第一个参数str表示需要进行url编码的字符串
 * 返回值是将str参数经过url编码后的结果字符串
 * 例如:
	use builtin;

	str = 'hello world!世界你好! 我是zengl!';

	print '\nurl encode:' + (enc_str = bltUrlEncode(str));

	上面代码片段会将str变量对应的字符串'hello world!世界你好! 我是zengl!'进行url编码,并将编码后的结果打印出来,执行结果如下:

	url encode:hello+world%21%E4%B8%96%E7%95%8C%E4%BD%A0%E5%A5%BD%EF%BC%81+%E6%88%91%E6%98%AFzengl%21

	url编码时,a-zA-Z0-9以及. - * _会保持不变,空格符会转为+号,其他字符会转为%加字节值的ASCII形式,例如0xE4字节会转为%E4等

	模块函数版本历史:
	 - v0.22.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_url_encode(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltUrlEncode";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(str): str", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [str] of %s must be string", func_name);
	}
	char * str = (char *)arg.val.str;
	int str_len = (int)strlen(str);
	int encode_size = sizeof(char) * str_len * 3 + 1;
	char * encode_str = (char *)zenglApi_AllocMem(VM_ARG, encode_size);
	memset(encode_str, 0, encode_size);
	const char * hex = "0123456789ABCDEF";
	int pos = 0;
	for (int i = 0; i < str_len; i++) {
		if (('a' <= str[i] && str[i] <= 'z')
			|| ('A' <= str[i] && str[i] <= 'Z')
			|| ('0' <= str[i] && str[i] <= '9')
			|| str[i] == '.' || str[i] == '-' || str[i] == '*' || str[i] == '_') {
				encode_str[pos++] = str[i];
			} else if (str[i] == ' ') {
				encode_str[pos++] = '+';
			}else {
				encode_str[pos++] = '%';
				encode_str[pos++] = hex[(unsigned char)str[i] >> 4];
				encode_str[pos++] = hex[(unsigned char)str[i] & 0xF];
			}
	}
	encode_str[pos] = '\0';
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, encode_str, 0, 0);
	zenglApi_FreeMem(VM_ARG, encode_str);
}

bltUrlDecode模块函数:

    当前版本还增加了bltUrlDecode模块函数,可以将字符串进行url解码,该模块函数也定义在module_builtin.c文件中:

/**
 * bltUrlDecode模块函数,将参数进行url解码
 * 第一个参数str必须是字符串类型,表示经过了url编码的字符串
 * 返回值是将参数str经过url解码后的字符串
 * 示例:
	use builtin;

	str = 'hello world!世界你好! 我是zengl!';

	print '\nurl encode:' + (enc_str = bltUrlEncode(str));

	print '\nurl decode:' + bltUrlDecode(enc_str) + '\n';

	上面代码片段会先将字符串'hello world!世界你好! 我是zengl!'进行url编码,再将其解码,并将编解码的结果打印出来,执行结果如下:

	url encode:hello+world%21%E4%B8%96%E7%95%8C%E4%BD%A0%E5%A5%BD%EF%BC%81+%E6%88%91%E6%98%AFzengl%21

	url decode:hello world!世界你好! 我是zengl!

	模块函数版本历史:
	 - v0.22.0版本新增此模块函数
 */
ZL_EXP_VOID module_builtin_url_decode(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	const char * func_name = "bltUrlDecode";
	if(argcount < 1)
		zenglApi_Exit(VM_ARG,"usage: %s(str): str", func_name);
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [str] of %s must be string", func_name);
	}
	char * str = (char *)arg.val.str;
	int str_len = (int)strlen(str);
	int decode_size = sizeof(char) * str_len + 1;
	char * decode_str = (char *)zenglApi_AllocMem(VM_ARG, decode_size);
	memset(decode_str, 0, decode_size);
	gl_request_url_decode(decode_str, str, str_len);
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_STR, decode_str, 0, 0);
	zenglApi_FreeMem(VM_ARG, decode_str);
}

bltOutputBlob模块函数输出字符串到客户端:

    当前版本对bltOutputBlob模块函数进行了调整,让其除了可以输出二进制数据到客户端外,还可以直接输出字符串,该模块函数也定义在module_builtin.c文件中:

/**
 * bltOutputBlob模块函数,直接将二进制数据输出到客户端
 * 该模块函数的第一个参数blob为字节指针,指向需要输出的二进制数据。第二个参数length表示二进制数据的字节大小
 *  从v0.22.0版本开始,第一个参数blob还可以是字符串类型,如果是字符串类型,则会将字符串输出到客户端,之前版本一直用print指令输出字符串到客户端,但是
 *  print指令会在末尾自动添加换行符,而bltOutputBlob模块函数在输出字符串到客户端时,只会原样输出字符串信息,不会在后面加换行符之类的
 *
 * 例如:
 * output = magickGetImageBlob(wand, &length); // 获取图像的二进制数据
 * rqtSetResponseHeader("Content-Type: image/" + magickGetImageFormat(wand));
 * bltOutputBlob(output, length); // 输出二进制数据
 *
 * 上面代码片段中,先通过magickGetImageBlob获取图像的二进制数据和二进制数据的长度(以字节为单位的大小),
 * 接着就可以通过bltOutputBlob模块函数将图像的二进制数据输出到客户端
 *
 * use builtin;
 * retval = 'success';
 * bltOutputBlob(retval, -1);
 *
 * 上面代码片段会将retval对应的字符串'success'输出到客户端,并且不会在末尾加换行符,也就是将参数原样输出
 * 当第二个参数小于0时,例如上面设置的-1时,该模块函数会自动根据第一个参数的长度来进行输出
 *
 * 模块函数版本历史:
 *  - v0.12.0版本新增此模块函数
 *  - v0.22.0版本中第一个参数可以是字符串类型,此外,当第二个参数小于0时,会自动判断第一个参数的长度
 */
ZL_EXP_VOID module_builtin_output_blob(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	if(argcount < 2)
		zenglApi_Exit(VM_ARG,"usage: bltOutputBlob(blob|str, length)");
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_INT && arg.type != ZL_EXP_FAT_STR) {
		zenglApi_Exit(VM_ARG,"the first argument [blob|str] of bltOutputBlob must be integer or string");
	}
	ZL_EXP_BOOL is_blob_ptr = ZL_EXP_TRUE;
	char * blob = NULL;
	if(arg.type == ZL_EXP_FAT_INT)
		blob = (char *)arg.val.integer;
	else {
		blob = arg.val.str;
		is_blob_ptr = ZL_EXP_FALSE;
	}
	MAIN_DATA * my_data = zenglApi_GetExtraData(VM_ARG, "my_data");
	int ptr_size = 0;
	if(is_blob_ptr) {
		int ptr_idx = pointer_list_get_ptr_idx(&(my_data->pointer_list), blob);
		if(ptr_idx < 0) {
			zenglApi_Exit(VM_ARG,"runtime error: the first argument [blob] of bltOutputBlob is invalid pointer");
		}
		ptr_size = my_data->pointer_list.list[ptr_idx].ptr_size;
	}
	else {
		ptr_size = (int)strlen(blob);
	}
	zenglApi_GetFunArg(VM_ARG,2,&arg);
	if(arg.type != ZL_EXP_FAT_INT) {
		zenglApi_Exit(VM_ARG,"the second argument [length] of bltOutputBlob must be integer");
	}
	int length = arg.val.integer;
	if(length < 0 || length > ptr_size) {
		length = ptr_size;
	}
	dynamic_string_append(&my_data->response_body, blob, length, RESPONSE_BODY_STR_SIZE);
	zenglApi_SetRetVal(VM_ARG, ZL_EXP_FAT_INT, ZL_EXP_NULL, length, 0);
}

curlEasySetopt模块函数增加SSL_VERIFYHOST选型:

    当前版本中,curlEasySetopt模块函数增加了SSL_VERIFYHOST选型,用于指示curl库是否校验当前域名与证书中的CN(common name)字段是不是匹配,该模块函数定义在module_curl.c文件中:

/**
 * curlEasySetopt模块函数,设置curl抓取相关的选项,例如:抓取的目标地址,需要使用的用户代理等
 * 该模块函数的第一个参数必须是有效的my_curl_handle_struct指针,该指针由curlEasyInit模块函数返回,
 * 第二个参数是字符串类型的选项名称,暂时只支持以下几个选项:
 * ...........................................................................................
 * 'SSL_VERIFYHOST':是否校验当前的域名与证书中的CN(common name)字段是不是匹配
 * ...........................................................................................
 * 第第二个参数是'SSL_VERIFYHOST'时,选项值必须是整数类型,表示是否校验当前的域名与证书中的CN(common name)字段是不是匹配,
 * 		SSL_VERIFYHOST的默认值是2(不是1,1在早期版本中只是用于调试,后面的版本已经去掉了1这个值),表示需要进行校验,如果不需要校验,可以将选项值设置为0
 * ...........................................................................................
 *
 * 模块函数版本历史:
 *  - v0.15.0版本新增此模块函数
 *  - v0.16.0版本增加了COOKIEFILE,COOKIEJAR,COOKIE,PROXY,POSTFIELDS,VERBOSE以及STDERR选项
 *  - v0.22.0版本增加了SSL_VERIFYHOST选项
 */
ZL_EXP_VOID module_curl_easy_setopt(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	if(argcount < 3)
		zenglApi_Exit(VM_ARG,"usage: curlEasySetopt(curl_handle, option_name, option_value[, option_value2]): integer");
	zenglApi_GetFunArg(VM_ARG,1,&arg);
	if(arg.type != ZL_EXP_FAT_INT) {
		zenglApi_Exit(VM_ARG,"the first argument [curl_handle] of curlEasySetopt must be integer");
	}
	my_curl_handle_struct * my_curl_handle = (my_curl_handle_struct *)arg.val.integer;
	...............................................................................................
}

    在v0_22_0/test.zl脚本中就用了该选型来关闭主机名校验。

opensslSign和opensslVerify模块函数增加use_evp的可选参数:

    当前版本的opensslSign和opensslVerify模块函数都增加了use_evp的可选参数,这两个模块函数都定义在module_openssl.c文件里:

/**
 * 以下代码参考自php的openssl扩展:https://github.com/php/php-src/blob/master/ext/openssl/openssl.c
 */
static EVP_MD * st_get_evp_md_from_algo(int algo)
{
	EVP_MD *mdtype;

	switch (algo) {
	case MOD_OPENSSL_NID_sha1:
		mdtype = (EVP_MD *) EVP_sha1();
		break;
	case MOD_OPENSSL_NID_ripemd160:
		mdtype = (EVP_MD *) EVP_ripemd160();
		break;
	case MOD_OPENSSL_NID_md5:
		mdtype = (EVP_MD *) EVP_md5();
		break;
	case MOD_OPENSSL_NID_sha256:
		mdtype = (EVP_MD *) EVP_sha256();
		break;
	case MOD_OPENSSL_NID_sha512:
		mdtype = (EVP_MD *) EVP_sha512();
		break;
	default:
		return NULL;
		break;
	}

	return mdtype;
}

................................................................................

/**
 * 下面C函数是 opensslSign 和 opensslVerify 模块函数的通用C代码
 * 第一个参数data表示RSA签名或验签的原数据,必须是字符串类型,或者是整数类型的指针
 * ................................................................................
 * 第七个参数use_evp也是可选参是,当提供了此参数时,必须是整数类型,表示在进行签名和验签时是否使用EVP_为前缀的底层库函数进行签名操作
 *  use_evp的默认值是0,表示使用默认的 RSA_sign 和 RSA_verify 的底层库函数来完成签名和验签操作
 *  当use_evp的值不为0时,则表示使用 EVP_SignFinal 和 EVP_VerifyFinal 的底层库函数来完成签名和验签操作
 *  当需要进行支付宝签名和验签时,需要使用EVP_为前缀的底层库函数来执行相关的底层操作,php语言的签名和验签的底层库函数也是用的EVP_为前缀的库函数
 * ................................................................................
 */
static void common_sign_verify(ZL_EXP_VOID * VM_ARG, ZL_EXP_INT argcount, const char * func_name,
		ZL_EXP_BOOL is_sign)
{
	ZENGL_EXPORT_MOD_FUN_ARG arg = {ZL_EXP_FAT_NONE,{0}};
	if(argcount < 5) {
		if(is_sign)
			zenglApi_Exit(VM_ARG,"usage: %s(data, data_len, private_key, &sigret, &siglen[, type = 0[, use_evp = 0]]): integer", func_name);
		else
			zenglApi_Exit(VM_ARG,"usage: %s(data, data_len, public_key, sigbuf, siglen[, type = 0[, use_evp = 0]]): integer", func_name);
	}
	................................................................................
	ZL_EXP_BOOL use_evp = ZL_EXP_FALSE;
	EVP_MD * mdtype = NULL;
	if(argcount > 5) {
		................................................................................
		if(argcount > 6) {
			zenglApi_GetFunArg(VM_ARG,7,&arg);
			if(arg.type != ZL_EXP_FAT_INT) {
				zenglApi_Exit(VM_ARG,"the seventh argument [use_evp] of %s must be integer", func_name);
			}
			int arg_use_evp = (int)arg.val.integer;
			use_evp = (arg_use_evp == 0) ? ZL_EXP_FALSE : ZL_EXP_TRUE;
		}
		if(use_evp) {
			mdtype = st_get_evp_md_from_algo(sign_type);
			if(mdtype == NULL) {
				zenglApi_Exit(VM_ARG,"the sixth argument [type] of %s is not supported when use evp", func_name, MODULE_OPENSSL_SIGN_TYPE);
			}
		}
	}
	if(sign_type == -1) {
		zenglApi_Exit(VM_ARG,"the sixth argument [type:%d] of %s is not supported", sign_type_idx, func_name);
	}
	if(is_sign) {
		int rsa_len = 0;
		if(use_evp)
			rsa_len = EVP_PKEY_size(evp_key);
		else
			rsa_len = RSA_size(rsa);
		unsigned char * sigret = (unsigned char *)zenglApi_AllocMem(VM_ARG, rsa_len);
		memset(sigret, 0, rsa_len);
		unsigned int siglen = 0;
		EVP_MD_CTX * md_ctx = NULL;
		int retval = 0;
		if(use_evp) {
			md_ctx = EVP_MD_CTX_create();
			if (md_ctx != NULL &&
						EVP_SignInit(md_ctx, mdtype) &&
						EVP_SignUpdate(md_ctx, data, data_len) &&
						EVP_SignFinal(md_ctx, sigret, &siglen, evp_key)) {
				retval = 1;
			}
			if(md_ctx != NULL) {
				EVP_MD_CTX_destroy(md_ctx);
			}
		} else {
			retval = RSA_sign(sign_type, data, data_len, sigret, &siglen, rsa);
		}
		................................................................................
	}
	else {
		................................................................................
		if(use_evp) {
			EVP_MD_CTX * md_ctx = NULL;
			int err = -1;
			md_ctx = EVP_MD_CTX_create();
			if (md_ctx != NULL &&
					EVP_VerifyInit (md_ctx, mdtype) &&
					EVP_VerifyUpdate (md_ctx, data, data_len)) {
				err = EVP_VerifyFinal(md_ctx, sigbuf, siglen, evp_key);
			}
			if(md_ctx != NULL) {
				EVP_MD_CTX_destroy(md_ctx);
			}
			zenglApi_SetRetVal(VM_ARG,ZL_EXP_FAT_INT, ZL_EXP_NULL, err, 0);
		} else {
			int retval = RSA_verify(sign_type, data, data_len, sigbuf, siglen, rsa);
			if(!retval) {
				zenglApi_SetRetVal(VM_ARG,ZL_EXP_FAT_INT, ZL_EXP_NULL, 0, 0);
			}
			else {
				zenglApi_SetRetVal(VM_ARG,ZL_EXP_FAT_INT, ZL_EXP_NULL, 1, 0);
			}
		}
	}
}

/**
 * opensslSign模块函数,用于生成RSA签名数据,和此模块函数相关的详细说明请参考 common_sign_verify 函数的注释
 *
 * 模块函数版本历史:
 *  - v0.20.0版本新增此模块函数
 *  - v0.22.0版本增加了use_evp的可选参数
 */
ZL_EXP_VOID module_openssl_sign(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	common_sign_verify(VM_ARG, argcount, "opensslSign", ZL_EXP_TRUE);
}

/**
 * opensslVerify模块函数,用于验证RSA签名数据,和此模块函数相关的详细说明请参考 common_sign_verify 函数的注释
 *
 * 模块函数版本历史:
 *  - v0.20.0版本新增此模块函数
 *  - v0.22.0版本增加了use_evp的可选参数
 */
ZL_EXP_VOID module_openssl_verify(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT argcount)
{
	common_sign_verify(VM_ARG, argcount, "opensslVerify", ZL_EXP_FALSE);
}

    当需要进行支付宝签名和验签时,需要使用EVP_为前缀的底层库函数来执行相关的底层操作,这时需要将opensslSign和opensslVerify模块函数的use_evp可选参数设置为不为0的值,以使用EVP_为前缀的底层库函数。

结束语:

    当前版本只在沙箱环境进行了支付测试,因为正式环境需要企业资质才能申请。此外,由于支付宝内部的支付算法在以后可能会调整,因此当前版本中的这些测试脚本的代码,在以后也可能需要进行相应的调整。

    天底下的路都是从无到有走出来的

—— 邓稼先

 

上下篇

下一篇: zenglServer v0.23.0 使用v1.8.3版本的zengl语言,增加bltTrim等模块函数,支持中文url路径等

上一篇: zenglServer v0.21.0 增加base64编解码相关的内建模块函数

相关文章

zenglServer v0.20.0 增加openssl加密解密相关的模块

zenglServer v0.8.0-v0.8.1

zenglServer v0.7.0-v0.7.1 mustache模板解析

zenglServer v0.15.0 - v0.15.1 增加curl模块,用于执行数据抓取操作

zenglServer v0.6.0 session会话

zenglServer v0.14.0 正则表达式模块