const yapi = require('../yapi.js'); const projectModel = require('../models/project.js'); const interfaceModel = require('../models/interface.js'); const mockExtra = require('../../common/mock-extra.js'); const { schemaValidator } = require('../../common/utils.js'); const _ = require('underscore'); const Mock = require('mockjs'); const variable = require('../../client/constants/variable.js') /** * * @param {*} apiPath /user/tom * @param {*} apiRule /user/:username */ function matchApi(apiPath, apiRule) { let apiRules = apiRule.split('/'); let apiPaths = apiPath.split('/'); let pathParams = { __weight: 0 }; if (apiPaths.length !== apiRules.length) { return false; } for (let i = 0; i < apiRules.length; i++) { if (apiRules[i]) { apiRules[i] = apiRules[i].trim(); } else { continue; } if ( apiRules[i].length > 2 && apiRules[i][0] === '{' && apiRules[i][apiRules[i].length - 1] === '}' ) { pathParams[apiRules[i].substr(1, apiRules[i].length - 2)] = apiPaths[i]; } else if (apiRules[i].indexOf(':') === 0) { pathParams[apiRules[i].substr(1)] = apiPaths[i]; } else if ( apiRules[i].length > 2 && apiRules[i].indexOf('{') > -1 && apiRules[i].indexOf('}') > -1 ) { let params = []; apiRules[i] = apiRules[i].replace(/\{(.+?)\}/g, function(src, match) { params.push(match); return '([^\\/\\s]+)'; }); apiRules[i] = new RegExp(apiRules[i]); if (!apiRules[i].test(apiPaths[i])) { return false; } let matchs = apiPaths[i].match(apiRules[i]); params.forEach((item, index) => { pathParams[item] = matchs[index + 1]; }); } else { if (apiRules[i] !== apiPaths[i]) { return false; }else{ pathParams.__weight++; } } } return pathParams; } function parseCookie(str) { if (!str || typeof str !== 'string') { return str; } if (str.split(';')[0]) { let c = str.split(';')[0].split('='); return { name: c[0], value: c[1] || '' }; } return null; } function handleCorsRequest(ctx) { let header = ctx.request.header; ctx.set('Access-Control-Allow-Origin', header.origin); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, HEADER, PATCH, OPTIONS'); ctx.set('Access-Control-Allow-Headers', header['access-control-request-headers']); ctx.set('Access-Control-Allow-Credentials', true); ctx.set('Access-Control-Max-Age', 1728000); ctx.body = 'ok'; } // 必填字段是否填写好 function mockValidator(interfaceData, ctx) { let i, j, l, len, noRequiredArr = []; let method = interfaceData.method.toUpperCase() || 'GET'; // query 判断 for (i = 0, l = interfaceData.req_query.length; i < l; i++) { let curQuery = interfaceData.req_query[i]; if (curQuery && typeof curQuery === 'object' && curQuery.required === '1') { if (!ctx.query[curQuery.name]) { noRequiredArr.push(curQuery.name); } } } // form 表单判断 if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'form') { for (j = 0, len = interfaceData.req_body_form.length; j < len; j++) { let curForm = interfaceData.req_body_form[j]; if (curForm && typeof curForm === 'object' && curForm.required === '1') { if ( ctx.request.body[curForm.name] || (ctx.request.body.fields && ctx.request.body.fields[curForm.name]) || (ctx.request.body.files && ctx.request.body.files[curForm.name]) ) { continue; } noRequiredArr.push(curForm.name); } } } let validResult; // json schema 判断 if (variable.HTTP_METHOD[method].request_body && interfaceData.req_body_type === 'json' && interfaceData.req_body_is_json_schema === true) { const schema = yapi.commons.json_parse(interfaceData.req_body_other); const params = yapi.commons.json_parse(ctx.request.body); validResult = schemaValidator(schema, params); } if (noRequiredArr.length > 0 || (validResult && !validResult.valid)) { let message = `错误信息:`; message += noRequiredArr.length > 0 ? `缺少必须字段 ${noRequiredArr.join(',')} ` : ''; message += validResult && !validResult.valid ? `schema 验证请求参数 ${validResult.message}` : ''; return { valid: false, message }; } return { valid: true }; } module.exports = async (ctx, next) => { // no used variable 'hostname' & 'config' // let hostname = ctx.hostname; // let config = yapi.WEBCONFIG; let path = ctx.path; let header = ctx.request.header; if (path.indexOf('/mock/') !== 0) { if (next) await next(); return true; } let paths = path.split('/'); let projectId = paths[2]; paths.splice(0, 3); path = '/' + paths.join('/'); ctx.set('Access-Control-Allow-Origin', header.origin); ctx.set('Access-Control-Allow-Credentials', true); // ctx.set('Access-Control-Allow-Origin', '*'); if (!projectId) { return (ctx.body = yapi.commons.resReturn(null, 400, 'projectId不能为空')); } let projectInst = yapi.getInst(projectModel), project; try { project = await projectInst.get(projectId); } catch (e) { return (ctx.body = yapi.commons.resReturn(null, 403, e.message)); } if (!project) { return (ctx.body = yapi.commons.resReturn(null, 400, '不存在的项目')); } let interfaceData, newpath; let interfaceInst = yapi.getInst(interfaceModel); try { newpath = path.substr(project.basepath.length); interfaceData = await interfaceInst.getByPath(project._id, newpath, ctx.method); let queryPathInterfaceData = await interfaceInst.getByQueryPath(project._id, newpath, ctx.method); //处理query_path情况 url 中有 ?params=xxx if (!interfaceData || interfaceData.length != queryPathInterfaceData.length) { let i, l, j, len, curQuery, match = false; for (i = 0, l = queryPathInterfaceData.length; i < l; i++) { match = false; let currentInterfaceData = queryPathInterfaceData[i]; curQuery = currentInterfaceData.query_path; if (!curQuery || typeof curQuery !== 'object' || !curQuery.path) { continue; } for (j = 0, len = curQuery.params.length; j < len; j++) { if (ctx.query[curQuery.params[j].name] !== curQuery.params[j].value) { continue; } if (j === len - 1) { match = true; } } if (match) { interfaceData = [currentInterfaceData]; break; } // if (i === l - 1) { // interfaceData = []; // } } } //处理动态路由 if (!interfaceData || interfaceData.length === 0) { let newData = await interfaceInst.getVar(project._id, ctx.method); let findInterface; let weight = 0; _.each(newData, item => { let m = matchApi(newpath, item.path); if (m !== false) { if(m.__weight >= weight){ findInterface = item; } delete m.__weight; ctx.request.query = Object.assign(m, ctx.request.query); return true; } return false; }); if (!findInterface) { //非正常跨域预检请求回应 if (ctx.method === 'OPTIONS' && ctx.request.header['access-control-request-method']) { return handleCorsRequest(ctx); } return (ctx.body = yapi.commons.resReturn( null, 404, `不存在的api, 当前请求path为 ${newpath}, 请求方法为 ${ ctx.method } ,请确认是否定义此请求。` )); } interfaceData = [await interfaceInst.get(findInterface._id)]; } if (interfaceData.length > 1) { return (ctx.body = yapi.commons.resReturn(null, 405, '存在多个api,请检查数据库')); } else { interfaceData = interfaceData[0]; } // 必填字段是否填写好 if (project.strice) { const validResult = mockValidator(interfaceData, ctx); if (!validResult.valid) { return (ctx.body = yapi.commons.resReturn( null, 404, `接口字段验证不通过, ${validResult.message}` )); } } let res; // mock 返回值处理 res = interfaceData.res_body; try { if (interfaceData.res_body_type === 'json') { if (interfaceData.res_body_is_json_schema === true) { //json-schema const schema = yapi.commons.json_parse(interfaceData.res_body); res = yapi.commons.schemaToJson(schema, { alwaysFakeOptionals: true }); } else { // console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data')) // 处理 format-data if ( _.isString(ctx.request.header['content-type']) && ctx.request.header['content-type'].indexOf('multipart/form-data') > -1 ) { ctx.request.body = ctx.request.body.fields; } // console.log('body', ctx.request.body) res = mockExtra(yapi.commons.json_parse(interfaceData.res_body), { query: ctx.request.query, body: ctx.request.body, params: Object.assign({}, ctx.request.query, ctx.request.body) }); // console.log('res',res) } try { res = Mock.mock(res); } catch (e) { console.log('err', e.message); yapi.commons.log(e, 'error'); } } let context = { projectData: project, interfaceData: interfaceData, ctx: ctx, mockJson: res, resHeader: {}, httpCode: 200, delay: 0 }; if (project.is_mock_open && project.project_mock_script) { // 项目层面的mock脚本解析 let script = project.project_mock_script; await yapi.commons.handleMockScript(script, context); } await yapi.emitHook('mock_after', context); let handleMock = new Promise(resolve => { setTimeout(() => { resolve(true); }, context.delay); }); await handleMock; if (context.resHeader && typeof context.resHeader === 'object') { for (let i in context.resHeader) { let cookie; if (i === 'Set-Cookie') { if (context.resHeader[i] && typeof context.resHeader[i] === 'string') { cookie = parseCookie(context.resHeader[i]); if (cookie && typeof cookie === 'object') { ctx.cookies.set(cookie.name, cookie.value, { maxAge: 864000000, httpOnly: false }); } } else if (context.resHeader[i] && Array.isArray(context.resHeader[i])) { context.resHeader[i].forEach(item => { cookie = parseCookie(item); if (cookie && typeof cookie === 'object') { ctx.cookies.set(cookie.name, cookie.value, { maxAge: 864000000, httpOnly: false }); } }); } } else { ctx.set(i, context.resHeader[i]); } } } ctx.status = context.httpCode; ctx.body = context.mockJson; return; } catch (e) { yapi.commons.log(e, 'error'); return (ctx.body = { errcode: 400, errmsg: '解析出错,请检查。Error: ' + e.message, data: null }); } } catch (e) { yapi.commons.log(e, 'error'); return (ctx.body = yapi.commons.resReturn(null, 409, e.message)); } };