Commit b10123ae authored by clauseliu(刘武)'s avatar clauseliu(刘武)
Browse files

修复安全漏洞,增加接口调试功能使用文档

parent e0ad3f08
Showing with 180 additions and 5737 deletions
+180 -5737
......@@ -53,7 +53,7 @@ app.use(helmet());
app.use(bodyparser());
app.use(upload.single('suse')); //这里决定了上传包的name只能叫suse。
app.use(upload.array('suse',5)); //这里决定了上传包的name只能叫suse。
//国际化多语言中间件
app.use(localeMidware);
......
......@@ -41,45 +41,54 @@ InfTestController.interfaceDebug = async (ctx) => {
}
} catch (e) {
logger.error('[interfaceDebug]:', e, ctx);
console.error(err);
ctx.makeResObj(500, err.message);
console.error(e);
ctx.makeResObj(500, e.message);
}
}
InfTestController.uploadTarsFile = async (ctx) => {
let {application, server_name, set_name} = ctx.paramsObj;
// tars文件上传目录,和发布包同一个根目录
let baseUploadPath = WebConf.pkgUploadPath.path;
let tarsFilePath = `${baseUploadPath}/tars_files/${application}/${server_name}`;
try {
let {application, server_name, set_name} = ctx.paramsObj;
await fs.ensureDirSync(tarsFilePath);
if (!await AuthService.hasDevAuth(application, server_name, ctx.uid)) {
ctx.makeNotAuthResObj();
} else {
let file = ctx.req.file;
if (!file) {
let files = ctx.req.files;
if (!files.length) {
logger.error('[uploadTarsFile]:', 'no files', ctx);
return ctx.makeErrResObj();
throw new Error('[uploadTarsFile]: no files');
}
if (!(/\.tars$/gi.test(file.originalname) && file.mimetype === 'application/octet-stream')) {
logger.error('[uploadTarsFile]:', 'only accept .tars files', ctx);
return ctx.makeResObj(500, 'only accept .tars files', null);
// 检查文件类型并重命名文件
for (let file of files) {
if (!(/\.tars$/gi.test(file.originalname) && file.mimetype === 'application/octet-stream')) {
logger.error('[uploadTarsFile]:', 'only accept .tars files', ctx);
throw new Error('[uploadTarsFile]: #pub.dlg.filetype#');
}
await fs.rename(`${baseUploadPath}/${file.filename}`, `${tarsFilePath}/${file.originalname}`);
}
// 解析并入库
let ret = [];
for (let file of files) {
const context = await InfTestService.getContext(`${tarsFilePath}/${file.originalname}`);
ret.push(await InfTestService.addTarsFile({
application: application,
server_name: server_name,
file_name: file.originalname,
context: JSON.stringify(context),
posttime: new Date()
}));
}
// tars文件上传目录,和发布包同一个根目录
let baseUploadPath = WebConf.pkgUploadPath.path;
let tarsFilePath = `${baseUploadPath}/tars_files/${application}/${server_name}`;
const context = await InfTestService.getContext(`${baseUploadPath}/${file.filename}`);
await fs.ensureDirSync(tarsFilePath);
await fs.rename(`${baseUploadPath}/${file.filename}`, `${tarsFilePath}/${file.originalname}`);
let ret = await InfTestService.addTarsFile({
application: application,
server_name: server_name,
file_name: file.originalname,
context: JSON.stringify(context),
posttime: new Date()
});
ctx.makeResObj(200, '', ret);
}
} catch (e) {
logger.error('[uploadTarsFile]:', e, ctx);
ctx.makeErrResObj();
} finally {
// 删除重命名后的文件
fs.remove(`${tarsFilePath}`);
}
}
......
......@@ -32,7 +32,7 @@ PatchController.uploadPatchPackage = async (ctx) => {
if (!await AuthService.hasDevAuth(application, module_name, ctx.uid)) {
ctx.makeNotAuthResObj();
} else {
let file = ctx.req.file;
let file = ctx.req.files[0];
if (!file) {
logger.error('[uploadPatchPackage]:', 'no files');
return ctx.makeErrResObj(500, 'no files');
......
......@@ -76,7 +76,8 @@ InfTestService.getContext = (tarsFilePath) => {
async function getContext(tarsFilePath) {
const content = await fs.readFile(tarsFilePath);
const parser = new TarsParser();
const fileDir = tarsFilePath.split(/[/\\]/).slice(0, -1).join('/');
const parser = new TarsParser(fileDir);
let context = {};
parser.parseFile(context, content.toString());
return context;
......
......@@ -34,7 +34,9 @@ const LEGAL_NAME = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/,
};
class TarsParser {
constructor() {}
constructor(fileDir) {
this.fileDir = fileDir;
}
parseFile(context, content) {
let tokenizer = new Tokenizer(content);
......@@ -53,11 +55,13 @@ class TarsParser {
if(tokenizer.next() != '{') {
throw Error(`missing { after moduleName at '${moduleName}'`);
}
context[moduleName] = {
enums : {},
structs : {},
interfaces : {},
consts : {}
if (!context[moduleName]) {
context[moduleName] = {
enums : {},
structs : {},
interfaces : {},
consts : {}
}
}
let token;
while ((tokenizer.peek() || "}") !== "}") {
......@@ -421,7 +425,7 @@ class TarsParser {
include = token.replace(/^['"]|['"]$/g,'');
context.includes = context.includes || {};
context.includes[include] = true;
this.parseFile(context, fs.readFileSync(path.join(__dirname, include)).toString());
this.parseFile(context, fs.readFileSync(path.join(this.fileDir, include)).toString());
}
}
......
......@@ -72,14 +72,14 @@
<let-uploader
:placeholder="$t('pub.dlg.filetype')"
accept=".tars"
@upload="uploadFile" require>
{{$t('common.submit')}}</let-uploader>
<span v-if="uploadModal.model.file">{{uploadModal.model.file.name}}</span>
multiple
@upload="uploadFile"
>{{$t('common.submit')}}</let-uploader>
<li v-for="item in uploadModal.fileList2Show" :key="item.index">{{item.name}}</li>
</let-form-item>
<let-button type="submit" theme="primary">{{$t('serverList.servant.upload')}}</let-button>
</let-form>
</let-modal>
</div>
</template>
......@@ -106,9 +106,9 @@ export default {
uploadModal : {
show : false,
model : {}
model : {},
fileList2Show: [],
},
showDebug : false,
contextData : [],
......@@ -119,7 +119,7 @@ export default {
selectedMethods: [],
objName : '',
objList : [],
selectedId : ''
selectedId : '',
}
},
methods: {
......@@ -142,20 +142,28 @@ export default {
application : this.serverData.application,
server_name : this.serverData.server_name,
set_name : this.serverData.set_name,
file : null
file : []
}
this.uploadModal.fileList2Show = [];
},
uploadFile(file) {
if (file.name) {
uploadFile(fileList) {
this.uploadModal.fileList2Show = [];
if (fileList.length) {
let len = 0;
for (let file of fileList) {
this.uploadModal.fileList2Show.push({name: file.name, index: Math.random()*100});
const arr = file.name.split('.');
const filetype = arr[arr.length - 1];
if (filetype === 'tars') {
this.uploadModal.model.file = file;
len += 1;
} else {
this.uploadModal.model.file = '';
break;
}
}
if (len === fileList.length) {
this.uploadModal.model.file = Array.from(fileList);
}
}
return this.uploadModal.model.file;
},
uploadTarsFile() {
if(this.$refs.uploadForm.validate()) {
......@@ -164,7 +172,7 @@ export default {
formdata.append('application', this.uploadModal.model.application);
formdata.append('server_name', this.uploadModal.model.server_name);
formdata.append('set_name', this.uploadModal.model.set_name);
formdata.append('suse', this.uploadModal.model.file);
this.uploadModal.model.file.forEach( file => formdata.append('suse', file));
this.$ajax.postForm('/server/api/upload_tars_file', formdata).then(() => {
loading.hide();
this.getFileList();
......@@ -280,7 +288,7 @@ export default {
}
}
});
this.inParam = JSON.stringify(obj);
this.inParam = JSON.stringify(obj, null, 4);
});
}).catch((err) => {
loading.hide();
......
......@@ -3,83 +3,107 @@
*
* Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
* Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
let request = require('request-promise-any');
var path = require('path');
/**
* 登录配置
*/
module.exports = {
enableLogin: false, //是否启用登录验证
defaultLoginUid: 'admin', //若不启用登录验证,默认用户为admin
loginUrl: 'http://localhost:3001/login.html', //登录跳转url
redirectUrlParamName: 'redirect_url', //跳转到登录url的时带的原url参数名,如:***/login?service=***,默认是service
logoutUrl: '',
logoutredirectUrlParamName: 'url',
ticketCookieName: 'ticket', //cookie中保存ticket信息的cookie名
uidCookieName: 'uid', //cookie中保存用户信息的cookie名
cookieDomain: 'localhost', //cookie值对应的域
ticketParamName: 'ticket', //第三方登录服务回调时候,url中表示st的参数名
getUidByTicket: getUidByTicket, //通过ticket从cas服务端校验和获取用户基本信息的url,或获取用户基本信息的方法
getUidByTicketParamName: 'ticket', //调用获取用户信息接口时候st的参数名
uidKey: 'data.uid', //结果JSON里面取出用户名的位置,取到该用户名才认为成功,可以多层
validate: validate, //通过token和用户名到cas服务端校验key和用户名是否匹配的url或方法
validateTicketParamName: 'ticket', //校验接口传入st参数名
validateUidParamName: 'uid', //校验接口传入用户参数名
validateMatch: [
['data.result', true]
], //校验通过匹配条件,可以从多层结果,多个情况
ignore: ['/static'], //不需要登录校验的路径
ignoreIps: [], //访问ip白名单
apiPrefix: ['/pages/server/api'], //接口相应的路径前缀,这类接口访问不直接跳转到登录界面,而只是提示未登录
apiNotLoginMes: '#common.noLogin#', //接口无登录权限的提示语
enableLogin: false, //是否启用登录验证
defaultLoginUid: 'admin', //若不启用登录验证,默认用户为admin
loginUrl: 'http://passport.oa.com/modules/passport/signin.ashx', //登录跳转url
redirectUrlParamName: 'url', //跳转到登录url的时带的原url参数名,如:***/login?service=***,默认是service
logoutUrl: 'http://passport.oa.com/modules/passport/signout.ashx',
logoutredirectUrlParamName: 'url',
ticketCookieName: 'ticket', //cookie中保存ticket信息的cookie名
uidCookieName: 'uid', //cookie中保存用户信息的cookie名
cookieDomain: 'wsd.com', //cookie值对应的域
ticketParamName: 'ticket', //第三方登录服务回调时候,url中表示st的参数名
// getUidByTicket: 'http://oss.api.tof.oa.com/api/v1/Passport/DecryptTicketWithClientIP', //通过ticket从cas服务端校验和获取用户基本信息的url
getUidByTicket: getUidByTicket, //通过ticket从cas服务端校验和获取用户基本信息的url,或获取用户基本信息的方法
getUidByTicketParamName: 'ticket', //调用获取用户信息接口时候st的参数名
uidKey: 'data.uid', //结果JSON里面取出用户名的位置,取到该用户名才认为成功,可以多层
validate: validate, //通过token和用户名到cas服务端校验key和用户名是否匹配的url或方法
validateTicketParamName: 'ticket', //校验接口传入st参数名
validateUidParamName: 'uid', //校验接口传入用户参数名
validateMatch: [
['data.result', true]
], //校验通过匹配条件,可以从多层结果,多个情况
ignore: ['/static', '/tarsnode.tar.gz'], //不需要登录校验的路径
ignoreIps: [], //访问ip白名单
apiPrefix: ['/pages/server/api'], //接口相应的路径前缀,这类接口访问不直接跳转到登录界面,而只是提示未登录
apiNotLoginMes: '#common.noLogin#', //接口无登录权限的提示语
};
var tof3 = require('@tencent/tof').tof3;
let _sysId = 26459;
let _appKey = 'dba24e450a144971a32f77c7454c94cb';
tof3.config({sysId: _sysId, appKey: _appKey});
tof3.setReqHost('oss.api.tof.oa.com');
/**
* 由用户直接定制通过ticket获取用户信息的方法
* @param ctx
*/
async function getUidByTicket(ctx, ticket) {
//TODO
return new Promise((resolve, reject) => {
try {
request.get('http://localhost:3001/api/getUidByTicket?ticket=' + ticket).then(uidInfo => {
uidInfo = JSON.parse(uidInfo);
resolve(uidInfo.data.uid);
}).catch(err => {
reject(err);
})
} catch (e) {
resolve(false)
}
})
async function getUidByTicket(ctx, ticket){
//TODO
return new Promise((resolve, reject)=>{
try{
tof3.passport.decryptTicketWithClientIP({ // 验证ticket的合法性
appkey: _appKey,
encryptedTicket: ticket,
browseIP: ctx.ip
}, function (err, data) {
if (err) {
resolve(false)
}
if (data) {
resolve(data.LoginName);
} else {
resolve(false)
}
});
}catch(e){
resolve(false)
}
})
}
/**
* 由用户直接定制判断用户名校验方法
* @param ctx
*/
async function validate(ctx, uid, ticket) {
//TODO
return new Promise((resolve, reject) => {
try {
request.get('http://localhost:3001/api/getUidByTicket?ticket=' + ticket).then(uidInfo => {
uidInfo = JSON.parse(uidInfo);
resolve(uidInfo.data.uid === uid);
}).catch(err => {
reject(err);
})
} catch (e) {
reject(false)
}
})
}
async function validate(ctx, uid, ticket){
//TODO
return new Promise((resolve, reject)=>{
try{
tof3.passport.decryptTicketWithClientIP({ // 验证ticket的合法性
appkey: _appKey,
encryptedTicket: ticket,
browseIP: ctx.ip
}, function (err, data) {
if (err) {
throw err;
}
if (data) {
resolve(data.LoginName === uid);
} else {
resolve(false)
}
});
}catch(e){
resolve(false)
}
})
}
\ No newline at end of file
......@@ -17,10 +17,10 @@ var path = require('path');
module.exports = {
dbConf: {
host: 'db.tars.com', // 数据库地址
host: 'localhost', // 数据库地址
port: '3306', // 数据库端口
user: 'tars', // 用户名
password: 'tars2015', // 密码
user: 'root', // 用户名
password: 'admin12345', // 密码
charset: 'utf8_bin', // 数据库编码
pool: {
max: 10, // 连接池中最大连接数量
......
docs/images/i1.png

5.09 KB

docs/images/i2.png

13.7 KB

docs/images/i3.png

14.1 KB

docs/images/i4.png

3.11 KB

# 接口调试功能使用文档
接口调试功能用于tars服务开发完成后,通过平台传参调试服务的可用性。
## 上传tars文件
点击添加按钮,添加服务的tars文件。如果tars文件有多个(一般分为描述文件和接口文件两个),需要同时上传(按住ctrl键选择多个文件)。
![上传tars文件](./images/i1.png)
## 调试
只要tars文件没有语法错误,一般都能上传成功。如果出现错误,请到后台日志中查看出错原因。
![tars文件列表](./images/i2.png)
上传成功后,点击tars接口文件后面的调试链接,进入到调试界面。
![调试界面](./images/i3.png)
选择需要调试的方法,入参框中会自动填入方法的入参。输入相应的值后点击调试即可。入参只解析到一层。如果入参是struct类型,可传入一个对象。
![入参](./images/i4.png)
\ No newline at end of file
This diff is collapsed.
{
"name": "Tars",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"scripts": {
"start": "node bin/www",
......@@ -42,6 +42,7 @@
"mysql2": "^1.5.1",
"node-schedule": "^1.3.0",
"node-ssh": "^5.1.1",
"pm2": "^4.1.2",
"request": "^2.87.0",
"request-promise-any": "^1.0.5",
"sequelize": "^4.37.10",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment