当前版本在my_webroot目录中增加了v0_22_0的子目录,该目录中存放了一些测试脚本,通过这些脚本可以实现支付宝的支付测试。
页面导航:
zenglServer源代码的相关地址:https://github.com/zenglong/zenglServer 当前版本对应的tag标签为: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页面,这些页面的输出结果都类似上面的交易查询结果,只是支付宝返回的数据不同而已。
上面的支付宝付款,交易查询,退款等入口页面,最终都会将请求提交给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, "'", "'"); 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>';
在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改成其他的字符串。
之前提到过,在执行付款操作时,如果支付成功,支付宝会跳转到商家的返回页面。在当前版本中,就是跳转到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模块函数,可以对字符串进行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模块函数,可以将字符串进行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模块函数进行了调整,让其除了可以输出二进制数据到客户端外,还可以直接输出字符串,该模块函数也定义在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_为前缀的底层库函数。
当前版本只在沙箱环境进行了支付测试,因为正式环境需要企业资质才能申请。此外,由于支付宝内部的支付算法在以后可能会调整,因此当前版本中的这些测试脚本的代码,在以后也可能需要进行相应的调整。
天底下的路都是从无到有走出来的
—— 邓稼先