增加应用下载授权token, 增加超级签名多重安全验证,优化本地下载逻辑

dependabot/npm_and_yarn/fir_admin/async-2.6.4
nineven 3 years ago
parent 3b8134aafd
commit 04ae8cccc4
  1. 33
      fir_client/src/components/FirDownload.vue
  2. 33
      fir_client/src/components/ShortDownload.vue
  3. 358
      fir_client/src/components/apps/FirAppInfossecurity.vue
  4. 32
      fir_client/src/components/apps/FirAppInfostimeline.vue
  5. 21
      fir_client/src/restful/download.js
  6. 17
      fir_client/src/restful/index.js
  7. 41
      fir_ser/api/migrations/0005_auto_20220412_1744.py
  8. 20
      fir_ser/api/models.py
  9. 3
      fir_ser/api/urls.py
  10. 14
      fir_ser/api/utils/modelutils.py
  11. 13
      fir_ser/api/utils/serializer.py
  12. 110
      fir_ser/api/views/apps.py
  13. 3
      fir_ser/api/views/download.py
  14. 9
      fir_ser/api/views/notify.py
  15. 9
      fir_ser/common/base/baseutils.py
  16. 10
      fir_ser/common/core/exception.py
  17. 3
      fir_ser/common/libs/sendmsg/templates/xsign/app_sign_failed.html
  18. 67
      fir_ser/common/utils/download.py
  19. 21
      fir_ser/common/utils/token.py
  20. 14
      fir_ser/xsign/utils/iossignapi.py
  21. 5
      fir_ser/xsign/utils/serializer.py
  22. 44
      fir_ser/xsign/utils/supersignutils.py
  23. 15
      fir_ser/xsign/views/download.py
  24. 123
      fir_ser/xsign/views/receiveudids.py
  25. 2
      mailhtml/src/app_sign_failed.mjml.html

@ -579,7 +579,8 @@ export default {
} }
} }
}, { }, {
"short": this.$route.params.short, methods: 'GET',
short: this.$route.params.short,
data: {"task_id": this.$route.query.task_id, "unique_key": unique_key} data: {"task_id": this.$route.query.task_id, "unique_key": unique_key}
}) })
}, },
@ -599,15 +600,27 @@ export default {
window.location.href = this.mobileprovision; window.location.href = this.mobileprovision;
}, },
check_msg() { check_msg() {
if (this.$route.query.udid) { if (this.$route.query.udid && this.$route.query.task_token) {
if (this.$route.query.task_id) { this.wrong = true;
this.wrong = true; this.msg = '签名处理中,请耐心等待';
this.msg = '签名处理中,请耐心等待'; gettask(data => {
this.loop_check_task() if (data.code === 1000) {
} else if (this.$route.query.msg) { if (data.task_id) {
this.wrong = true; this.$route.query.task_id = data.task_id;
this.show_err_msg(this.$route.query.msg); this.loop_check_task()
} } else if (data.result) {
this.show_err_msg(data.result);
} else {
this.wrong = false;
}
} else {
this.show_err_msg(data.msg);
}
}, {
methods: 'POST',
short: this.$route.params.short,
data: {"task_token": this.$route.query.task_token}
})
} }
}, },
download() { download() {

@ -544,7 +544,8 @@ export default {
} }
} }
}, { }, {
"short": this.$route.params.short, methods: 'GET',
short: this.$route.params.short,
data: {"task_id": this.$route.query.task_id, "unique_key": unique_key} data: {"task_id": this.$route.query.task_id, "unique_key": unique_key}
}) })
}, },
@ -564,15 +565,27 @@ export default {
window.location.href = this.mobileprovision; window.location.href = this.mobileprovision;
}, },
check_msg() { check_msg() {
if (this.$route.query.udid) { if (this.$route.query.udid && this.$route.query.task_token) {
if (this.$route.query.task_id) { this.wrong = true;
this.wrong = true; this.msg = '签名处理中,请耐心等待';
this.msg = '签名处理中,请耐心等待'; gettask(data => {
this.loop_check_task() if (data.code === 1000) {
} else if (this.$route.query.msg) { if (data.task_id) {
this.wrong = true; this.$route.query.task_id = data.task_id;
this.show_err_msg(this.$route.query.msg); this.loop_check_task()
} } else if (data.result) {
this.show_err_msg(data.result);
} else {
this.wrong = false;
}
} else {
this.show_err_msg(data.msg);
}
}, {
methods: 'POST',
short: this.$route.params.short,
data: {"task_token": this.$route.query.task_token}
})
} }
}, },
download() { download() {

@ -9,14 +9,169 @@
<bind-domain v-if="bind_domain_sure" :app_id="this.currentapp.app_id" :c_domain_name="this.currentapp.domain_name" <bind-domain v-if="bind_domain_sure" :app_id="this.currentapp.app_id" :c_domain_name="this.currentapp.domain_name"
:domain_type="2" transitionName="bind-app-domain"/> :domain_type="2" transitionName="bind-app-domain"/>
</el-dialog> </el-dialog>
<el-form label-width="80px"> <el-dialog
<el-form-item label="访问密码" label-width="200px"> :close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="download_password_sure"
center
title="应用下载授权码设置"
width="800px">
<el-dialog
:visible.sync="makeTokenVisible"
append-to-body
center
title="下载授权码生成"
width="500px">
<el-form ref="form" :model="makeTokenInfo" label-width="110px">
<el-form-item label="指定授权码" style="width: 400px">
<el-tag>当存在指定授权码授权码长度和生成数量被禁用</el-tag>
<el-input v-model="makeTokenInfo.token" clearable placeholder="输入指定下载授权码"></el-input>
</el-form-item>
<el-form-item label="授权码长度">
<el-input-number v-model="makeTokenInfo.token_length" :disabled="tokendisable" :max="32"
:min="4"></el-input-number>
<el-tag style="margin-left: 5px">授权码长度为4-32之间</el-tag>
</el-form-item>
<el-form-item label="生成数量">
<el-input-number v-model="makeTokenInfo.token_number" :disabled="tokendisable" :max="1024"
:min="1" :step="20"></el-input-number>
<el-tag style="margin-left: 5px">单次最多可生成1024个</el-tag>
</el-form-item>
<el-form-item label="最大使用次数">
<el-input-number v-model="makeTokenInfo.token_max_used_number" :max="1024" :min="0"></el-input-number>
<el-tag style="margin-left: 5px">0表示不限使用次数</el-tag>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="cancelDownloadToken">取消</el-button>
<el-button @click="makeDownloadToken">生成</el-button>
</span>
</el-dialog>
<el-input
v-model="dpwdsearch"
clearable
placeholder="输入下载授权码"
style="width: 30%;margin-right: 30px;margin-bottom: 10px"/>
<el-button icon="el-icon-search" type="primary" @click="handleSearch(1)">
搜索
</el-button>
<div style="float: right;width: 400px;text-align: right">
<el-button style="margin:0 10px 5px" @click="makeTokenVisible=true">
生成下载授权码
</el-button>
<el-button plain style="margin: 0 10px 5px" type="warning" @click="cleanToken('all')">清空所有授权码</el-button>
<el-button plain style="margin: 5px 10px 5px" type="warning" @click="cleanToken('invalid')">清理无效授权码</el-button>
<el-button plain style="margin: 5px 10px 5px" type="warning" @click="cleanToken('some')">清理选中授权码</el-button>
</div>
<el-table
v-loading="loading"
:data="app_download_token_list"
border
stripe
style="width: 100%"
@selection-change="tokenHandleSelectionChange">
<el-table-column
align="center"
type="selection"
width="55">
</el-table-column>
<el-table-column
align="center"
label="授权码"
prop="token"
>
<template slot-scope="scope">
<el-tooltip content="点击复制">
<el-link v-clipboard:copy="format_copy_text(scope.row.token)" v-clipboard:success="copy_success"
:underline="false">
{{ scope.row.token }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
:formatter="tokenformatter"
align="center"
label="生成日期"
prop="create_time"
width="160">
</el-table-column>
<el-table-column
align="center"
label="使用次数"
prop="used_count"
width="100"
>
<template slot-scope="scope">
<span v-if="scope.row.max_limit_count === 0">不限次数</span>
<span v-else>{{ scope.row.used_count }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
label="最大可用次数"
prop="max_limit_count"
width="140">
<template slot-scope="scope">
<span v-if="scope.row.max_limit_count === 0">不限次数</span>
<span v-else>{{ scope.row.max_limit_count }}</span>
</template>
</el-table-column>
<el-table-column
align="center"
fixed="right"
label="操作"
width="120">
<template slot-scope="scope">
<div>
<el-tooltip content="重置使用次数" placement="top">
<el-button
size="mini"
@click="resetDownloadUsed(scope.row)">重置
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 20px">
<el-pagination
:current-page.sync="pagination.currentPage"
:page-size="pagination.pagesize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total,sizes, prev, pager, next"
@size-change="tokenHandleSizeChange"
@current-change="tokenHandleCurrentChange">
</el-pagination>
</div>
<span slot="footer">
<el-button @click="closeDownloadTokenInfo">关闭</el-button>
</span>
</el-dialog>
<el-form label-width="80px">
<el-form-item label="下载授权码" label-width="200px">
<el-tooltip placement="top"> <el-tooltip placement="top">
<div slot="content"> <div slot="content">
{{ passwordtip.msg }}<br> {{ passwordtip.msg }}<br>
<div v-if="passwordtip.val === 'on'"> <div v-if="passwordtip.val === 'on'">
<el-link :underline="false" icon="el-icon-edit" @click="setaccesspassword">修改</el-link> <el-link :underline="false" icon="el-icon-edit" @click="handleSearch(1)">查看配置下载授权码</el-link>
</div> </div>
</div> </div>
<el-switch <el-switch
@ -29,7 +184,12 @@
</el-switch> </el-switch>
</el-tooltip> </el-tooltip>
<el-link :underline="false" style="margin-left: 20px">设置密码之后用户需要输入密码才可以下载该应用</el-link> <el-popover
placement="top"
trigger="hover">
<el-link :underline="false" icon="el-icon-edit" @click="handleSearch(1)">查看配置下载授权码</el-link>
<el-link slot="reference" :underline="false" style="margin-left: 20px">设置授权码之后用户需要输入授权码才可以下载该应用</el-link>
</el-popover>
</el-form-item> </el-form-item>
@ -93,9 +253,10 @@
</template> </template>
<script> <script>
import {apputils,} from "@/restful" import {appDownloadToken, apputils,} from "@/restful"
import {deepCopy} from "@/utils"; import {deepCopy} from "@/utils";
import BindDomain from "@/components/base/BindDomain"; import BindDomain from "@/components/base/BindDomain";
import {format_time} from "@/utils/base/utils";
export default { export default {
name: "FirAppInfossecurity", name: "FirAppInfossecurity",
@ -115,9 +276,124 @@ export default {
wxeasy_disable: false, wxeasy_disable: false,
defualt_dtitle: '专属下载页域名', defualt_dtitle: '专属下载页域名',
bind_domain_sure: false, bind_domain_sure: false,
download_password_sure: false,
app_download_token_list: [],
loading: false,
makeTokenVisible: false,
tokendisable: false,
pagination: {"currentPage": 1, "total": 0, "pagesize": 10},
dpwdsearch: '',
makeTokenInfo: {token: '', token_length: 6, token_number: 20, token_max_used_number: 0},
multipleSelection: []
} }
}, },
methods: { methods: {
format_copy_text(token) {
let short_full_url = this.currentapp.preview_url + "/" + this.currentapp.short;
return "应用链接:" + short_full_url + " 下载授权码:" + token
},
copy_success() {
this.$message.success('复制剪切板成功');
},
cleanToken(act) {
appDownloadToken(data => {
this.loading = false
if (data.code === 1000) {
this.$message.success("操作成功")
this.handleSearch(1)
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'PUT',
app_id: this.currentapp.app_id,
data: {'act': act, 'tokens': this.format_token(this.multipleSelection)}
})
},
format_token(token_info_list) {
let format_token_list = []
for (let token_info of token_info_list) {
format_token_list.push(token_info.token)
}
return format_token_list
},
tokenHandleSelectionChange(val) {
this.multipleSelection = val;
},
cancelDownloadToken() {
this.makeTokenInfo = {token: '', token_length: 6, token_number: 20, token_max_used_number: 0}
this.makeTokenVisible = false
},
resetDownloadUsed(info) {
appDownloadToken(data => {
this.loading = false
if (data.code === 1000) {
this.$message.success("重置成功")
this.showDownloadBase()
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'PUT', app_id: this.currentapp.app_id, data: {'token': info.token}
})
},
makeDownloadToken() {
appDownloadToken(data => {
this.loading = false
if (data.code === 1000) {
this.cancelDownloadToken()
this.$message.success("下载授权码生成成功")
this.showDownloadBase()
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'POST', app_id: this.currentapp.app_id, data: this.makeTokenInfo
})
},
handleSearch(val) {
this.pagination.currentPage = val;
this.showDownloadBase()
},
showDownloadBase() {
this.showDownloadToken({
"size": this.pagination.pagesize,
"page": this.pagination.currentPage,
"dpwdsearch": this.dpwdsearch.replace(/^\s+|\s+$/g, "")
})
},
showDownloadToken(data) {
this.loading = true
appDownloadToken(data => {
this.loading = false
if (data.code === 1000) {
this.download_password_sure = true;
this.app_download_token_list = data.data
this.pagination.total = data.count
} else {
this.$message.error("获取失败了 " + data.msg)
}
}, {
methods: 'GET', app_id: this.currentapp.app_id, data: data
})
},
closeDownloadTokenInfo() {
this.download_password_sure = false;
},
tokenHandleSizeChange(val) {
this.pagination.pagesize = val;
this.showDownloadBase();
},
tokenHandleCurrentChange(val) {
this.pagination.currentPage = val;
this.showDownloadBase();
},
// eslint-disable-next-line no-unused-vars
tokenformatter(row, column) {
return format_time(row.create_time)
},
set_default_flag() { set_default_flag() {
this.passwordflag = false; this.passwordflag = false;
this.showdownloadflag = false; this.showdownloadflag = false;
@ -144,7 +420,7 @@ export default {
}); });
}, },
setbuttondefaltpass(currentapp) { setbuttondefaltpass(currentapp) {
if (currentapp.password === '') { if (currentapp.need_password === false) {
this.passwordtip.val = 'off'; this.passwordtip.val = 'off';
this.showpasswordevent("off"); this.showpasswordevent("off");
} else { } else {
@ -191,57 +467,6 @@ export default {
this.setxeasytypeshow(currentapp); this.setxeasytypeshow(currentapp);
this.setwxredirectshow(currentapp); this.setwxredirectshow(currentapp);
}, },
passwordswitch(state) {
this.passwordflag = false;
this.showpasswordevent(state);
this.passwordtip.val = state;
this.passwordflag = true;
},
setaccesspassword() {
this.$prompt('', '请设置访问密码', {
confirmButtonText: '确定',
cancelButtonText: '取消',
closeOnClickModal: false,
inputValue: `${this.currentapp.password}`,
}).then(({value}) => {
value = value.replace(/\s+/g, "");
if (this.currentapp.password === value) {
if (value === '') {
this.$message({
type: 'success',
message: '访问密码未变'
});
this.passwordflag = false;
this.setbuttondefaltpass(this.currentapp);
return
} else {
return
}
}
this.saveappinfo({
"password": value,
});
if (value === '') {
this.passwordswitch("off");
this.$message({
type: 'success',
message: '设置成功,取消密码访问'
});
} else {
this.passwordtip.msg = '访问密码:' + value;
this.$message({
type: 'success',
message: '设置成功,访问密码是: ' + value
});
}
this.currentapp.password = value;
this.$store.dispatch('doucurrentapp', this.currentapp)
}).catch(() => {
if (this.currentapp.password === '') {
this.passwordswitch("off")
}
});
},
showdownloadevent(newval) { showdownloadevent(newval) {
if (newval === "on") { if (newval === "on") {
if (this.showdownloadflag) { if (this.showdownloadflag) {
@ -264,19 +489,21 @@ export default {
showpasswordevent(newval) { showpasswordevent(newval) {
if (newval === "on") { if (newval === "on") {
if (this.passwordflag) { if (this.passwordflag) {
this.setaccesspassword() this.saveappinfo({
} else { "need_password": 1,
this.passwordtip.msg = '访问密码:' + this.currentapp.password; });
this.currentapp.need_password = 1;
} }
this.passwordtip.msg = '已经开启授权码下载功能'
} else { } else {
if (this.passwordflag) { if (this.passwordflag) {
this.saveappinfo({ this.saveappinfo({
"password": '', "need_password": 0,
}); });
this.currentapp.need_password = 0;
} }
this.currentapp.password = ''; this.passwordtip.msg = '未开启授权码下载功能'
this.$store.dispatch('doucurrentapp', this.currentapp);
this.passwordtip.msg = '无访问密码'
} }
}, },
@ -342,6 +569,9 @@ export default {
'$store.state.currentapp': function () { '$store.state.currentapp': function () {
this.appinit(); this.appinit();
}, },
'makeTokenInfo.token': function () {
this.tokendisable = !!(this.makeTokenInfo.token && this.makeTokenInfo.token.length > 0);
}
}, computed: {} }, computed: {}
} }
</script> </script>

@ -103,7 +103,7 @@
</template> </template>
<script> <script>
import {getdownloadurl, releaseapputils} from "@/restful" import {releaseapputils} from "@/restful"
export default { export default {
name: "FirAppInfostimeline", name: "FirAppInfostimeline",
@ -126,22 +126,20 @@ export default {
} }
}, },
downloadPackage(app) { downloadPackage(app) {
getdownloadurl(res => {
if (res.code === 1000) { releaseapputils(res => {
window.location.href = res.data.download_url; if (res.code === 1000) {
} else { window.location.href = res.data.download_url;
this.$message.error(res.msg); } else {
} this.$message.error(res.msg);
}, { }
'data': { }, {
'token': app.download_token, methods: 'POST',
'short': this.currentapp.short, app_id: this.currentapp.app_id,
'release_id': app.release_id, release_id: app.release_id,
"password": this.currentapp.password, data: {'token': app.download_token, 'short': this.currentapp.short}
"isdownload": true, }
}, );
'app_id': this.currentapp.app_id
})
}, },
previewRelease(app) { previewRelease(app) {
let routeData = this.$router.resolve({ let routeData = this.$router.resolve({

@ -35,23 +35,6 @@ function ErrorMsg(error) {
function getData(methods = 'GET', url, params = {}, callBack) { function getData(methods = 'GET', url, params = {}, callBack) {
var uri = '';
var keys = Object.keys(params);
var values = Object.values(params);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = values[i];
uri = uri + key + "=" + value;
if (i < keys.length - 1) {
uri = uri + "&"
}
}
if (uri !== "") {
uri = "?" + uri
}
url = url + uri;
if (methods === "PUT") { if (methods === "PUT") {
Axios Axios
.put(url, params) .put(url, params)
@ -74,7 +57,7 @@ function getData(methods = 'GET', url, params = {}, callBack) {
}); });
} else } else
Axios Axios
.get(url, params) .get(url, {params: params})
.then(function (response) { .then(function (response) {
callBack(response.data); callBack(response.data);
let x = ''; let x = '';
@ -165,7 +148,7 @@ let SIGNSEVER = DOMAIN + '/api/v1/fir/xsign';
export function gettask(callBack, params) { export function gettask(callBack, params) {
getData( getData(
'GET', params.methods,
SIGNSEVER + '/task/' + params.short, SIGNSEVER + '/task/' + params.short,
params.data, params.data,
data => { data => {

@ -396,6 +396,21 @@ export function releaseapputils(callBack, params, load = true) {
); );
} }
/**应用下载token操作 */
export function appDownloadToken(callBack, params, load = true) {
getData(
params.methods,
USERSEVER + '/download_password/' + params.app_id,
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}
/**根据短链接获取应用信息 */ /**根据短链接获取应用信息 */
export function getShortAppinfo(callBack, params, load = true) { export function getShortAppinfo(callBack, params, load = true) {
@ -951,7 +966,7 @@ export function signoperatemessage(callBack, params, load = true) {
/**获取签名任务状态 */ /**获取签名任务状态 */
export function gettask(callBack, params, load = true) { export function gettask(callBack, params, load = true) {
getData( getData(
'GET', params.methods,
SIGNSEVER + '/task/' + params.short, SIGNSEVER + '/task/' + params.short,
params.data, params.data,
data => { data => {

@ -0,0 +1,41 @@
# Generated by Django 3.2.3 on 2022-04-12 17:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0004_auto_20220330_0009'),
]
operations = [
migrations.RemoveField(
model_name='apps',
name='password',
),
migrations.AddField(
model_name='apps',
name='need_password',
field=models.BooleanField(default=False, help_text='默认 没有密码', verbose_name='访问密码'),
),
migrations.CreateModel(
name='AppDownloadToken',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('token', models.CharField(max_length=64, verbose_name='授权码')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='添加时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('used_count', models.BigIntegerField(default=0, verbose_name='已经使用次数')),
('max_limit_count', models.BigIntegerField(default=0, verbose_name='最大可使用次数,0表示不限制')),
('description', models.CharField(blank=True, default='', max_length=256, verbose_name='备注')),
('app_id',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.apps', verbose_name='应用信息')),
],
options={
'verbose_name': '应用下载授权token',
'verbose_name_plural': '应用下载授权token',
'unique_together': {('app_id', 'token')},
},
),
]

@ -153,7 +153,7 @@ class Apps(models.Model):
verbose_name="关联应用", on_delete=models.SET_NULL, null=True, blank=True) verbose_name="关联应用", on_delete=models.SET_NULL, null=True, blank=True)
created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
count_hits = models.BigIntegerField(verbose_name="下载次数", default=0) count_hits = models.BigIntegerField(verbose_name="下载次数", default=0)
password = models.CharField(verbose_name="访问密码", blank=True, help_text='默认 没有密码', max_length=32) need_password = models.BooleanField(verbose_name="访问密码", help_text='默认 没有密码', default=False)
isshow = models.BooleanField(verbose_name="下载页可见", default=True) isshow = models.BooleanField(verbose_name="下载页可见", default=True)
issupersign = models.BooleanField(verbose_name="是否超级签名包", default=False) issupersign = models.BooleanField(verbose_name="是否超级签名包", default=False)
change_auto_sign = models.BooleanField(verbose_name="签名相关的数据更新自动签名", default=False) change_auto_sign = models.BooleanField(verbose_name="签名相关的数据更新自动签名", default=False)
@ -529,3 +529,21 @@ class NotifyConfig(models.Model):
def __str__(self): def __str__(self):
return "%s-%s-%s" % (self.user_id, self.get_message_type_display(), self.sender) return "%s-%s-%s" % (self.user_id, self.get_message_type_display(), self.sender)
class AppDownloadToken(models.Model):
app_id = models.ForeignKey(to=Apps, on_delete=models.CASCADE, verbose_name="应用信息")
token = models.CharField(max_length=64, verbose_name='授权码')
create_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
used_count = models.BigIntegerField(verbose_name="已经使用次数", default=0)
max_limit_count = models.BigIntegerField(verbose_name="最大可使用次数,0表示不限制", default=0)
description = models.CharField(verbose_name="备注", max_length=256, default='', blank=True)
class Meta:
verbose_name = '应用下载授权token'
verbose_name_plural = "应用下载授权token"
unique_together = (('app_id', 'token',),)
def __str__(self):
return "%s-%s-%s" % (self.app_id, self.token, self.description)

@ -16,7 +16,7 @@ Including another URLconf
from django.urls import re_path from django.urls import re_path
from api.views.advert import UserAdInfoView from api.views.advert import UserAdInfoView
from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView, AppsQrcodeShowView from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView, AppsQrcodeShowView, AppDownloadTokenView
from api.views.domain import DomainCnameView, DomainInfoView from api.views.domain import DomainCnameView, DomainInfoView
from api.views.download import ShortDownloadView, InstallView, DownloadView from api.views.download import ShortDownloadView, InstallView, DownloadView
from api.views.getip import GetRemoteIp from api.views.getip import GetRemoteIp
@ -48,6 +48,7 @@ urlpatterns = [
re_path("^storage$", StorageView.as_view()), re_path("^storage$", StorageView.as_view()),
re_path("^storage/clean$", CleanStorageView.as_view()), re_path("^storage/clean$", CleanStorageView.as_view()),
re_path(r"^apps/(?P<app_id>\w+)", AppInfoView.as_view()), re_path(r"^apps/(?P<app_id>\w+)", AppInfoView.as_view()),
re_path(r"^download_password/(?P<app_id>\w+)", AppDownloadTokenView.as_view()),
re_path(r"^appinfos/(?P<app_id>\w+)/(?P<act>\w+)", AppReleaseInfoView.as_view()), re_path(r"^appinfos/(?P<app_id>\w+)/(?P<act>\w+)", AppReleaseInfoView.as_view()),
re_path("^upload$", UploadView.as_view()), re_path("^upload$", UploadView.as_view()),
re_path("^userinfo", UserInfoView.as_view()), re_path("^userinfo", UserInfoView.as_view()),

@ -13,7 +13,7 @@ from django.db.models import Count
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from api.models import AppReleaseInfo, UserDomainInfo, DomainCnameInfo, UserAdDisplayInfo, RemoteClientInfo, \ from api.models import AppReleaseInfo, UserDomainInfo, DomainCnameInfo, UserAdDisplayInfo, RemoteClientInfo, \
AppBundleIdBlackList, NotifyReceiver, WeChatInfo AppBundleIdBlackList, NotifyReceiver, WeChatInfo, AppDownloadToken
from common.base.baseutils import get_server_domain_from_request, get_user_default_domain_name, get_real_ip_address, \ from common.base.baseutils import get_server_domain_from_request, get_user_default_domain_name, get_real_ip_address, \
get_origin_domain_name get_origin_domain_name
@ -228,3 +228,15 @@ def get_wx_nickname(openid):
if obj: if obj:
nickname = obj.nickname nickname = obj.nickname
return nickname if nickname else '亲爱哒' return nickname if nickname else '亲爱哒'
def check_app_access_token(app_id, access_token):
download_token_obj = AppDownloadToken.objects.filter(app_id__app_id=app_id,
token=access_token.upper()).first()
if download_token_obj:
if download_token_obj.max_limit_count == 0:
return True
download_token_obj.used_count += 1
if download_token_obj.used_count <= download_token_obj.max_limit_count:
download_token_obj.save(update_fields=['used_count'])
return True

@ -267,13 +267,6 @@ class AppsShortSerializer(serializers.ModelSerializer):
def get_domain_name(self, obj): def get_domain_name(self, obj):
return get_app_domain_name(obj) return get_app_domain_name(obj)
need_password = serializers.SerializerMethodField()
def get_need_password(self, obj):
if obj.password != '':
return True
return False
has_combo = serializers.SerializerMethodField() has_combo = serializers.SerializerMethodField()
def get_has_combo(self, obj): def get_has_combo(self, obj):
@ -508,3 +501,9 @@ class NotifyConfigSerializer(serializers.ModelSerializer):
def get_senders(self, obj): def get_senders(self, obj):
return NotifyReceiverSerializer(obj.sender, many=True).data return NotifyReceiverSerializer(obj.sender, many=True).data
class AppDownloadTokenSerializer(serializers.ModelSerializer):
class Meta:
model = models.AppDownloadToken
exclude = ["id", "app_id"]

@ -3,25 +3,29 @@
# project: 3月 # project: 3月
# author: liuyu # author: liuyu
# date: 2020/3/4 # date: 2020/3/4
import copy
import logging import logging
from django.db.models import Sum from django.db.models import Sum, F
from django.db.models.functions import Length
from rest_framework.pagination import PageNumberPagination from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from api.base_views import app_delete from api.base_views import app_delete
from api.models import Apps, AppReleaseInfo, UserInfo, AppScreenShot from api.models import Apps, AppReleaseInfo, UserInfo, AppScreenShot, AppDownloadToken
from api.utils.modelutils import get_user_domain_name, get_app_domain_name from api.utils.modelutils import get_user_domain_name, get_app_domain_name
from api.utils.response import BaseResponse from api.utils.response import BaseResponse
from api.utils.serializer import AppsSerializer, AppReleaseSerializer, AppsListSerializer, AppsQrListSerializer from api.utils.serializer import AppsSerializer, AppReleaseSerializer, AppsListSerializer, AppsQrListSerializer, \
AppDownloadTokenSerializer
from api.utils.signalutils import run_delete_app_signal from api.utils.signalutils import run_delete_app_signal
from api.utils.utils import delete_local_files, delete_app_screenshots_files from api.utils.utils import delete_local_files, delete_app_screenshots_files
from common.cache.state import MigrateStorageState from common.cache.state import MigrateStorageState
from common.core.auth import ExpiringTokenAuthentication from common.core.auth import ExpiringTokenAuthentication
from common.utils.caches import del_cache_response_by_short, get_app_today_download_times, del_cache_by_delete_app from common.utils.caches import del_cache_response_by_short, get_app_today_download_times, del_cache_by_delete_app
from common.utils.download import get_app_download_url
from common.utils.storage import Storage from common.utils.storage import Storage
from common.utils.token import verify_token, get_random_download_token
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -172,9 +176,9 @@ class AppInfoView(APIView):
app_obj.description = data.get("description", app_obj.description) app_obj.description = data.get("description", app_obj.description)
app_obj.short = data.get("short", app_obj.short) app_obj.short = data.get("short", app_obj.short)
app_obj.name = data.get("name", app_obj.name) app_obj.name = data.get("name", app_obj.name)
app_obj.password = data.get("password", app_obj.password) app_obj.need_password = data.get("need_password", app_obj.need_password)
app_obj.isshow = data.get("isshow", app_obj.isshow) app_obj.isshow = data.get("isshow", app_obj.isshow)
update_fields = ["description", "short", "name", "password", "isshow"] update_fields = ["description", "short", "name", "need_password", "isshow"]
if get_user_domain_name(request.user) or get_app_domain_name(app_obj): if get_user_domain_name(request.user) or get_app_domain_name(app_obj):
app_obj.wxeasytype = data.get("wxeasytype", app_obj.wxeasytype) app_obj.wxeasytype = data.get("wxeasytype", app_obj.wxeasytype)
else: else:
@ -313,6 +317,23 @@ class AppReleaseInfoView(APIView):
return Response(res.dict) return Response(res.dict)
def post(self, request, app_id, act):
res = BaseResponse()
downtoken = request.data.get("token", '')
short = request.data.get("short", '')
if not downtoken or not short or not act or not app_id:
res.code = 1004
res.msg = "参数丢失"
return Response(res.dict)
if verify_token(downtoken, act):
res = get_app_download_url(request, res, app_id, short, None, act, True, '')
else:
res.code = 1004
res.msg = "token校验失败"
return Response(res.dict)
class AppsQrcodeShowView(APIView): class AppsQrcodeShowView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ] authentication_classes = [ExpiringTokenAuthentication, ]
@ -330,3 +351,80 @@ class AppsQrcodeShowView(APIView):
res.data = app_serializer.data res.data = app_serializer.data
res.has_next = page_obj.page.has_next() res.has_next = page_obj.page.has_next()
return Response(res.dict) return Response(res.dict)
class AppDownloadTokenView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request, app_id):
res = BaseResponse()
dpwdsearch = request.query_params.get('dpwdsearch')
app_token_queryset = AppDownloadToken.objects.filter(app_id__user_id=request.user, app_id__app_id=app_id)
if dpwdsearch:
app_token_queryset = app_token_queryset.filter(token=dpwdsearch)
page_obj = AppsPageNumber()
app_token_page_serializer = page_obj.paginate_queryset(queryset=app_token_queryset.order_by("-create_time"),
request=request, view=self)
app_token_serializer = AppDownloadTokenSerializer(app_token_page_serializer, many=True)
res.data = app_token_serializer.data
res.count = app_token_queryset.count()
return Response(res.dict)
def post(self, request, app_id):
res = BaseResponse()
data = request.data
token = data.get('token')
token_length = data.get('token_length')
token_number = data.get('token_number')
token_max_used_number = data.get('token_max_used_number')
app_obj = Apps.objects.filter(user_id=request.user, app_id=app_id).first()
if token and isinstance(token, str) and len(token) >= 4:
AppDownloadToken.objects.update_or_create(app_id=app_obj, token=token,
defaults={"max_limit_count": token_max_used_number})
else:
if isinstance(token_length, int) and 4 <= token_length <= 32:
pass
else:
token_length = 6
if isinstance(token_number, int) and 1 <= token_number <= 1024:
pass
else:
token_number = 20
download_token_queryset = AppDownloadToken.objects.filter(app_id=app_obj).annotate(
token_len=Length('token')).filter(token_len=token_length).values('token').all()
exist_token = [d_token['token'] for d_token in download_token_queryset]
make_token_list = get_random_download_token(token_length=token_length,
token_number=token_number + download_token_queryset.count(),
exist_token=copy.deepcopy(exist_token))
bulk_list = []
for d_token in list(set(make_token_list) - set(exist_token))[:token_number]:
bulk_list.append(
AppDownloadToken(token=d_token, max_limit_count=token_max_used_number, app_id=app_obj))
AppDownloadToken.objects.bulk_create(bulk_list)
return Response(res.dict)
def put(self, request, app_id):
res = BaseResponse()
token = request.data.get('token')
act = request.data.get('act')
if token is not None:
AppDownloadToken.objects.filter(app_id__user_id=request.user,
app_id__app_id=app_id, token=token).update(used_count=0)
if act:
if act == 'all':
AppDownloadToken.objects.filter(app_id__user_id=request.user, app_id__app_id=app_id).delete()
elif act == 'invalid':
AppDownloadToken.objects.filter(app_id__user_id=request.user,
app_id__app_id=app_id,
used_count__gte=F('max_limit_count'), max_limit_count__gt=0).delete()
elif act == 'some':
tokens = request.data.get('tokens')
if tokens and isinstance(tokens, list):
AppDownloadToken.objects.filter(app_id__user_id=request.user,
app_id__app_id=app_id, token__in=tokens).delete()
return Response(res.dict)

@ -169,7 +169,6 @@ class InstallView(APIView):
downtoken = query_params.get("token", None) downtoken = query_params.get("token", None)
short = query_params.get("short", None) short = query_params.get("short", None)
release_id = query_params.get("release_id", None) release_id = query_params.get("release_id", None)
isdownload = query_params.get("isdownload", None)
password = query_params.get("password", None) password = query_params.get("password", None)
udid = query_params.get("udid", None) udid = query_params.get("udid", None)
@ -179,7 +178,7 @@ class InstallView(APIView):
return Response(res.dict) return Response(res.dict)
if verify_token(downtoken, release_id): if verify_token(downtoken, release_id):
res = get_app_download_url(request, res, app_id, short, password, release_id, isdownload, udid) res = get_app_download_url(request, res, app_id, short, password, release_id, False, udid)
else: else:
res.code = 1004 res.code = 1004
res.msg = "token校验失败" res.msg = "token校验失败"

@ -18,6 +18,7 @@ from api.utils.response import BaseResponse
from api.utils.serializer import NotifyReceiverSerializer, NotifyConfigSerializer from api.utils.serializer import NotifyReceiverSerializer, NotifyConfigSerializer
from api.views.login import check_common_info from api.views.login import check_common_info
from common.base.baseutils import get_choices_dict, get_choices_name_from_key, is_valid_email, is_valid_phone from common.base.baseutils import get_choices_dict, get_choices_name_from_key, is_valid_email, is_valid_phone
from common.cache.storage import NotifyLoopCache
from common.core.auth import ExpiringTokenAuthentication from common.core.auth import ExpiringTokenAuthentication
from common.core.sysconfig import Config from common.core.sysconfig import Config
from common.utils.caches import login_auth_failed from common.utils.caches import login_auth_failed
@ -74,6 +75,8 @@ class NotifyConfigView(APIView):
request.user.notify_available_signs = notify_available_signs request.user.notify_available_signs = notify_available_signs
if notify_available_downloads is not None or notify_available_signs is not None: if notify_available_downloads is not None or notify_available_signs is not None:
request.user.save(update_fields=['notify_available_downloads', 'notify_available_signs']) request.user.save(update_fields=['notify_available_downloads', 'notify_available_signs'])
NotifyLoopCache(request.user.uid, 'download_times').del_storage_cache()
NotifyLoopCache(request.user.uid, 'sign_device_times').del_storage_cache()
res.data = UserInfo.objects.filter(pk=request.user.pk).values('notify_available_downloads', res.data = UserInfo.objects.filter(pk=request.user.pk).values('notify_available_downloads',
'notify_available_signs').first() 'notify_available_signs').first()
@ -105,6 +108,12 @@ class NotifyConfigView(APIView):
notify_config_obj.enable_email = enable_email notify_config_obj.enable_email = enable_email
if enable_weixin is not None or enable_email is not None: if enable_weixin is not None or enable_email is not None:
notify_config_obj.save(update_fields=['enable_email', 'enable_weixin']) notify_config_obj.save(update_fields=['enable_email', 'enable_weixin'])
if notify_config_obj.message_type == 0:
NotifyLoopCache(request.user.uid, 'sign_device_times').del_storage_cache()
elif notify_config_obj.message_type == 1:
NotifyLoopCache(request.user.uid, 'download_times').del_storage_cache()
elif notify_config_obj.message_type == 6:
NotifyLoopCache(request.user.uid, 'developer_cert').del_storage_cache()
return Response(res.dict) return Response(res.dict)

@ -276,15 +276,6 @@ def get_format_time():
return now.strftime('%Y-%m-%d_%H:%M:%S') return now.strftime('%Y-%m-%d_%H:%M:%S')
def check_app_password(app_password, password):
if app_password != '':
if password is None:
return None
if app_password.lower() != password.strip().lower():
return None
return True
def get_real_ip_address(request): def get_real_ip_address(request):
if request.META.get('HTTP_X_FORWARDED_FOR', None): if request.META.get('HTTP_X_FORWARDED_FOR', None):
return request.META.get('HTTP_X_FORWARDED_FOR') return request.META.get('HTTP_X_FORWARDED_FOR')

@ -5,6 +5,7 @@
# date: 2022/1/13 # date: 2022/1/13
import logging import logging
from rest_framework.exceptions import Throttled
from rest_framework.views import exception_handler from rest_framework.views import exception_handler
from common.core.response import ApiResponse from common.core.response import ApiResponse
@ -17,6 +18,15 @@ def common_exception_handler(exc, context):
ret = exception_handler(exc, context) # 是Response对象,它内部有个data ret = exception_handler(exc, context) # 是Response对象,它内部有个data
logger.error(f'{context["view"].__class__.__name__} ERROR: {exc} ret:{ret}') logger.error(f'{context["view"].__class__.__name__} ERROR: {exc} ret:{ret}')
if isinstance(exc, Throttled):
second = f' {exc.wait} 秒之后'
if not second:
second = '稍后'
ret.data = {
'code': 999,
'detail': f'您手速太快啦,请{second}再次访问',
}
if not ret: # drf内置处理不了,丢给django 的,我们自己来处理 if not ret: # drf内置处理不了,丢给django 的,我们自己来处理
return ApiResponse(msg='error', result=str(exc), code=5000) return ApiResponse(msg='error', result=str(exc), code=5000)
else: else:

@ -226,8 +226,7 @@
style="font-size:0px;padding:10px 25px;word-break:break-word;"> style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#525252;"> <div style="font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;line-height:1;text-align:left;color:#525252;">
失败原因:开发者 {{ developer_obj.issuer_id }} 状态 失败原因:开发者 {{ developer_obj.issuer_id }} 状态
{developer_obj.get_status_display()} {{ developer_obj.get_status_display }}</div>
</div>
</td> </td>
</tr> </tr>
<tr> <tr>

@ -9,9 +9,9 @@ import os
import time import time
from api.models import Apps, UserInfo from api.models import Apps, UserInfo
from api.utils.modelutils import get_app_d_count_by_app_id, add_remote_info_from_request from api.utils.modelutils import get_app_d_count_by_app_id, add_remote_info_from_request, check_app_access_token
from api.utils.signalutils import run_xsign_app_download_url from api.utils.signalutils import run_xsign_app_download_url
from common.base.baseutils import check_app_password, get_real_ip_address from common.base.baseutils import get_real_ip_address
from common.cache.storage import AppDownloadTodayTimesCache, AppDownloadTimesCache, DownloadUrlCache, AppInstanceCache from common.cache.storage import AppDownloadTodayTimesCache, AppDownloadTimesCache, DownloadUrlCache, AppInstanceCache
from common.core.sysconfig import Config from common.core.sysconfig import Config
from common.utils.caches import consume_user_download_times from common.utils.caches import consume_user_download_times
@ -20,9 +20,18 @@ from common.utils.storage import Storage, LocalStorage
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_download_url_by_cache(app_obj, filename, limit, isdownload=True, key='', udid=None): def get_download_url_by_cache(app_obj, filename, limit, is_download=True, key='', udid=None):
now = time.time() now = time.time()
if isdownload is None: if is_download:
download_val = DownloadUrlCache(key, filename).get_storage_cache()
if download_val:
if download_val.get("time") > now - 60:
return download_val.get("download_url"), ""
else:
user_obj = UserInfo.objects.filter(pk=app_obj.get("user_id")).first()
storage = Storage(user_obj)
return storage.get_download_url(filename, limit), ""
else:
local_storage = LocalStorage(**Config.IOS_PMFILE_DOWNLOAD_DOMAIN) local_storage = LocalStorage(**Config.IOS_PMFILE_DOWNLOAD_DOMAIN)
download_url_type = 'plist' download_url_type = 'plist'
if not udid: if not udid:
@ -52,43 +61,36 @@ def get_download_url_by_cache(app_obj, filename, limit, isdownload=True, key='',
mobileconifg = local_storage.get_download_url(filename.split(".")[0] + "." + "mobileprovision", limit) mobileconifg = local_storage.get_download_url(filename.split(".")[0] + "." + "mobileprovision", limit)
return local_storage.get_download_url(filename.split(".")[0] + "." + download_url_type, limit), mobileconifg return local_storage.get_download_url(filename.split(".")[0] + "." + download_url_type, limit), mobileconifg
download_val = DownloadUrlCache(key, filename).get_storage_cache()
if download_val:
if download_val.get("time") > now - 60:
return download_val.get("download_url"), ""
else:
user_obj = UserInfo.objects.filter(pk=app_obj.get("user_id")).first()
storage = Storage(user_obj)
return storage.get_download_url(filename, limit), ""
def get_app_instance_by_cache(app_id, password, limit, udid): def get_app_instance_by_cache(app_id, password, is_download, limit, udid):
if udid: if udid:
app_info = Apps.objects.filter(app_id=app_id).values("pk", 'user_id', 'type', 'password', 'issupersign', app_info = Apps.objects.filter(app_id=app_id).values("pk", 'user_id', 'type', 'need_password', 'issupersign',
'user_id__certification__status').first() 'user_id__certification__status').first()
if app_info: if app_info:
app_info['d_count'] = get_app_d_count_by_app_id(app_id) app_info['d_count'] = get_app_d_count_by_app_id(app_id)
app_password = app_info.get("password") need_password = app_info.get("need_password")
if not check_app_password(app_password, password): if need_password and not is_download:
return None if not check_app_access_token(app_id, password):
return app_info return False, '下载授权码有误'
return True, app_info
app_instance_cache = AppInstanceCache(app_id) app_instance_cache = AppInstanceCache(app_id)
app_obj_cache = app_instance_cache.get_storage_cache() app_obj_cache = app_instance_cache.get_storage_cache()
if not app_obj_cache: if not app_obj_cache:
app_obj_cache = Apps.objects.filter(app_id=app_id).values("pk", 'user_id', 'type', 'password', app_obj_cache = Apps.objects.filter(app_id=app_id).values("pk", 'user_id', 'type', 'need_password',
'issupersign', 'issupersign',
'user_id__certification__status').first() 'user_id__certification__status').first()
if app_obj_cache: if app_obj_cache:
app_obj_cache['d_count'] = get_app_d_count_by_app_id(app_id) app_obj_cache['d_count'] = get_app_d_count_by_app_id(app_id)
app_instance_cache.set_storage_cache(app_obj_cache, limit) app_instance_cache.set_storage_cache(app_obj_cache, limit)
if not app_obj_cache: if not app_obj_cache:
return None return False, '应用不存在'
app_password = app_obj_cache.get("password") need_password = app_obj_cache.get("need_password")
if need_password and not is_download:
if not check_app_password(app_password, password): if not check_app_access_token(app_id, password):
return None return False, '下载授权码有误'
return app_obj_cache return True, app_obj_cache
def set_app_today_download_times(app_id): def set_app_today_download_times(app_id):
@ -112,19 +114,16 @@ def set_app_download_by_cache(app_id, limit=900):
return download_times + 1 return download_times + 1
def get_app_download_url(request, res, app_id, short, password, release_id, isdownload, udid): def get_app_download_url(request, res, app_id, short, password, release_id, is_download, udid):
app_obj = get_app_instance_by_cache(app_id, password, 900, udid) status, app_obj = get_app_instance_by_cache(app_id, password, is_download, 900, udid)
if app_obj: if status:
if app_obj.get("type") == 0: if app_obj.get("type") == 0:
app_type = '.apk' app_type = '.apk'
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600) download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600)
else: else:
app_type = '.ipa' app_type = '.ipa'
if isdownload: download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, is_download,
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, udid=udid) udid=udid)
else:
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, isdownload,
udid=udid)
res.data = {"download_url": download_url, "extra_url": extra_url} res.data = {"download_url": download_url, "extra_url": extra_url}
if download_url != "" and "mobileconifg" not in download_url: if download_url != "" and "mobileconifg" not in download_url:
@ -145,5 +144,5 @@ def get_app_download_url(request, res, app_id, short, password, release_id, isdo
return res return res
return res return res
res.code = 1006 res.code = 1006
res.msg = "该应用不存在" res.msg = str(app_obj)
return res return res

@ -72,3 +72,24 @@ def generate_numeric_token_of_length(length, random_str=''):
def generate_alphanumeric_token_of_length(length): def generate_alphanumeric_token_of_length(length):
return "".join( return "".join(
[random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for _ in range(length)]) [random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for _ in range(length)])
def generate_good_token_of_length(length):
ascii_uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
digits = '23456789'
return "".join([random.choice(digits + ascii_uppercase) for _ in range(length)])
def get_random_download_token(token_length=4, token_number=1024, exist_token=None):
if exist_token is None:
exist_token = []
count = 256
random_list = []
while count > 0:
random_list.append(generate_good_token_of_length(token_length))
count -= 1
exist_token.extend(random_list)
exist_token = list(set(exist_token))
if len(exist_token) >= token_number:
return exist_token
return get_random_download_token(token_length, token_number, exist_token)

@ -75,8 +75,10 @@ class ResignApp(object):
self.cmd = "zsign -c '%s' -k '%s' " % (self.app_dev_pem, self.my_local_key) self.cmd = "zsign -c '%s' -k '%s' " % (self.app_dev_pem, self.my_local_key)
@staticmethod @staticmethod
def sign_mobile_config(sign_data, ssl_pem_path, ssl_key_path): def sign_mobile_config(sign_data, ssl_pem_path, ssl_key_path, ssl_pem_data=None, ssl_key_data=None):
""" """
:param ssl_key_data:
:param ssl_pem_data:
:param sign_data: 签名的数据 :param sign_data: 签名的数据
:param ssl_pem_path: pem证书的绝对路径 :param ssl_pem_path: pem证书的绝对路径
:param ssl_key_path: key证书的绝对路径 :param ssl_key_path: key证书的绝对路径
@ -92,8 +94,14 @@ class ResignApp(object):
from cryptography.hazmat.primitives.serialization import pkcs7 from cryptography.hazmat.primitives.serialization import pkcs7
from cryptography import x509 from cryptography import x509
try: try:
if not ssl_key_data:
ssl_key_data = open(ssl_key_path, 'rb').read()
if not ssl_pem_data:
ssl_pem_data = open(ssl_pem_path, 'rb').read()
cert_list = re.findall('-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', cert_list = re.findall('-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----',
open(ssl_pem_path, 'r').read(), re.S) ssl_pem_data.decode('utf-8'), re.S)
if len(cert_list) == 0: if len(cert_list) == 0:
raise Exception('load cert failed') raise Exception('load cert failed')
else: else:
@ -101,7 +109,7 @@ class ResignApp(object):
cas = [cert] cas = [cert]
if len(cert_list) > 1: if len(cert_list) > 1:
cas.extend([x509.load_pem_x509_certificate(x.encode('utf-8')) for x in cert_list[1:]]) cas.extend([x509.load_pem_x509_certificate(x.encode('utf-8')) for x in cert_list[1:]])
key = serialization.load_pem_private_key(open(ssl_key_path, 'rb').read(), None) key = serialization.load_pem_private_key(ssl_key_data, None)
result['data'] = pkcs7.PKCS7SignatureBuilder( result['data'] = pkcs7.PKCS7SignatureBuilder(
data=sign_data.encode('utf-8'), data=sign_data.encode('utf-8'),
signers=[ signers=[

@ -330,11 +330,6 @@ class AdminBillInfoSerializer(BillInfoSerializer):
read_only_fields = ["id", "user_id", "to_user_id", "action", "number", "app_info", "udid", read_only_fields = ["id", "user_id", "to_user_id", "action", "number", "app_info", "udid",
"udid_sync_info", "app_id", "remote_addr"] "udid_sync_info", "app_id", "remote_addr"]
action_choices = serializers.SerializerMethodField()
def get_action_choices(self, obj):
return get_choices_dict(obj.action_choices)
class AppSignSerializer(serializers.ModelSerializer): class AppSignSerializer(serializers.ModelSerializer):
class Meta: class Meta:

@ -9,6 +9,7 @@ import os
import time import time
import uuid import uuid
import zipfile import zipfile
from io import BytesIO
import xmltodict import xmltodict
from django.core.cache import cache from django.core.cache import cache
@ -18,9 +19,10 @@ from api.models import UserInfo, AppReleaseInfo, Apps
from api.utils.response import BaseResponse from api.utils.response import BaseResponse
from api.utils.utils import delete_local_files, download_files_form_oss from api.utils.utils import delete_local_files, download_files_form_oss
from common.base.baseutils import file_format_path, delete_app_profile_file, get_profile_full_path, format_apple_date, \ from common.base.baseutils import file_format_path, delete_app_profile_file, get_profile_full_path, format_apple_date, \
get_format_time, make_app_uuid, make_from_user_uuid get_format_time, make_app_uuid, make_from_user_uuid, AesBaseCrypt
from common.base.magic import run_function_by_locker, call_function_try_attempts, magic_wrapper from common.base.magic import run_function_by_locker, call_function_try_attempts, magic_wrapper
from common.cache.state import CleanErrorBundleIdSignDataState from common.cache.state import CleanErrorBundleIdSignDataState
from common.cache.storage import RedisCacheBase
from common.constants import DeviceStatus, AppleDeveloperStatus, SignStatus from common.constants import DeviceStatus, AppleDeveloperStatus, SignStatus
from common.core.sysconfig import Config from common.core.sysconfig import Config
from common.notify.notify import sign_failed_notify, sign_unavailable_developer_notify, sign_app_over_limit_notify from common.notify.notify import sign_failed_notify, sign_unavailable_developer_notify, sign_app_over_limit_notify
@ -109,30 +111,27 @@ def udid_bytes_to_dict(xml_stream):
return new_uuid_info return new_uuid_info
def make_sign_udid_mobile_config(udid_url, short, bundle_id, app_name): def make_sign_udid_mobile_config(udid_url, bundle_id, app_name):
if Config.MOBILE_CONFIG_SIGN_SSL.get("open"): if Config.MOBILE_CONFIG_SIGN_SSL.get("open"):
ssl_key_path = Config.MOBILE_CONFIG_SIGN_SSL.get("ssl_key_path", None) ssl_key_path = Config.MOBILE_CONFIG_SIGN_SSL.get("ssl_key_path", None)
ssl_pem_path = Config.MOBILE_CONFIG_SIGN_SSL.get("ssl_pem_path", None) ssl_pem_path = Config.MOBILE_CONFIG_SIGN_SSL.get("ssl_pem_path", None)
ssl_key_cache = RedisCacheBase(AesBaseCrypt().get_encrypt_uid(ssl_key_path))
if ssl_key_path and ssl_pem_path and os.path.isfile(ssl_key_path) and os.path.isfile(ssl_pem_path): ssl_pem_cache = RedisCacheBase(AesBaseCrypt().get_encrypt_uid(ssl_pem_path))
mobile_config_tmp_dir = os.path.join(SUPER_SIGN_ROOT, 'tmp', 'mobile_config') ssl_key_data = ssl_key_cache.get_storage_cache()
if not os.path.exists(mobile_config_tmp_dir): ssl_pem_data = ssl_pem_cache.get_storage_cache()
os.makedirs(mobile_config_tmp_dir) if not ssl_key_data or not ssl_pem_data:
if ssl_key_path and ssl_pem_path and os.path.isfile(ssl_key_path) and os.path.isfile(ssl_pem_path):
sign_mobile_config_path = os.path.join(mobile_config_tmp_dir, 'sign_' + short) ssl_key_cache.set_storage_cache(open(ssl_key_path, 'rb').read(), 24 * 3600)
logger.info(f"make sing mobile config {sign_mobile_config_path}") ssl_pem_cache.set_storage_cache(open(ssl_pem_path, 'rb').read(), 24 * 3600)
if os.path.isfile(sign_mobile_config_path): ssl_key_data = ssl_key_cache.get_storage_cache()
return open(sign_mobile_config_path, 'rb') ssl_pem_data = ssl_pem_cache.get_storage_cache()
status, result = ResignApp.sign_mobile_config( if ssl_key_data and ssl_pem_data:
make_udid_mobile_config(udid_url, bundle_id, app_name), status, result = ResignApp.sign_mobile_config(make_udid_mobile_config(udid_url, bundle_id, app_name),
ssl_pem_path, ssl_pem_path, ssl_key_path, ssl_pem_data, ssl_key_data)
ssl_key_path)
if status and result.get('data'): if status and result.get('data'):
with open(sign_mobile_config_path, 'wb') as f: buffer = BytesIO(result.get("data"))
f.write(result.get('data')) return buffer
return open(sign_mobile_config_path, 'rb')
else: else:
logger.error( logger.error(
f"{bundle_id} {app_name} sign_mobile_config failed ERROR:{result.get('err_info')}") f"{bundle_id} {app_name} sign_mobile_config failed ERROR:{result.get('err_info')}")
@ -397,7 +396,6 @@ class IosUtils(object):
self.udid = udid_info.get('udid') self.udid = udid_info.get('udid')
self.app_obj = app_obj self.app_obj = app_obj
self.user_obj = user_obj self.user_obj = user_obj
self.get_developer_auth()
def get_developer_auth(self, read_only=True): def get_developer_auth(self, read_only=True):
self.developer_obj = get_developer_obj_by_others(self.user_obj, self.udid, self.app_obj, read_only) self.developer_obj = get_developer_obj_by_others(self.user_obj, self.udid, self.app_obj, read_only)
@ -537,7 +535,7 @@ class IosUtils(object):
res = check_app_permission(self.app_obj, BaseResponse()) res = check_app_permission(self.app_obj, BaseResponse())
if res.code != 1000: if res.code != 1000:
return False, {'code': res.code, 'msg': res.msg} return False, {'code': res.code, 'msg': res.msg}
self.get_developer_auth(True)
if not self.developer_obj: if not self.developer_obj:
msg = "udid %s app %s not exists apple developer" % (self.udid, self.app_obj) msg = "udid %s app %s not exists apple developer" % (self.udid, self.app_obj)
d_result['code'] = 1005 d_result['code'] = 1005

@ -6,6 +6,7 @@
import logging import logging
import os import os
import random
from django.urls import reverse from django.urls import reverse
from rest_framework.views import APIView from rest_framework.views import APIView
@ -15,7 +16,7 @@ from common.base.baseutils import get_profile_full_path, make_random_uuid, get_s
from common.core.response import ApiResponse, file_response, mobileprovision_file_response from common.core.response import ApiResponse, file_response, mobileprovision_file_response
from common.core.sysconfig import Config from common.core.sysconfig import Config
from common.utils.storage import Storage, get_local_storage from common.utils.storage import Storage, get_local_storage
from common.utils.token import verify_token from common.utils.token import verify_token, make_token
from fir_ser import settings from fir_ser import settings
from xsign.models import APPToDeveloper, APPSuperSignUsedInfo from xsign.models import APPToDeveloper, APPSuperSignUsedInfo
from xsign.utils.supersignutils import make_sign_udid_mobile_config from xsign.utils.supersignutils import make_sign_udid_mobile_config
@ -23,9 +24,11 @@ from xsign.utils.supersignutils import make_sign_udid_mobile_config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_post_udid_url(request, short): def get_post_udid_url(request, app_obj):
server_domain = get_server_domain_from_request(request, Config.POST_UDID_DOMAIN) server_domain = get_server_domain_from_request(request, Config.POST_UDID_DOMAIN)
return f'{server_domain}{reverse("xudid", kwargs={"short": short})}' p_token = make_token(app_obj.app_id, time_limit=120, key='post_udid', force_new=True)
token = f'{p_token}{"".join(random.sample(p_token, 3))}{app_obj.app_id}{"".join(random.sample(p_token, 3))}'
return f'{server_domain}{reverse("xudid", kwargs={"short": app_obj.short})}?p={token}'
class XsignDownloadView(APIView): class XsignDownloadView(APIView):
@ -69,10 +72,8 @@ class XsignDownloadView(APIView):
release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first() release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first()
if release_obj: if release_obj:
app_obj = release_obj.app_id app_obj = release_obj.app_id
udid_url = get_post_udid_url(request, app_obj.short) udid_url = get_post_udid_url(request, app_obj)
ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, f'{app_obj.app_id}_{app_obj.short}', ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, app_obj.bundle_id, app_obj.name)
app_obj.bundle_id,
app_obj.name)
return file_response(ios_udid_mobile_config, make_random_uuid() + '.mobileconfig', return file_response(ios_udid_mobile_config, make_random_uuid() + '.mobileconfig',
"application/x-apple-aspen-config") "application/x-apple-aspen-config")
return ApiResponse(code=1004, msg="mobile_config release_id error") return ApiResponse(code=1004, msg="mobile_config release_id error")

@ -3,7 +3,9 @@
# project: 3月 # project: 3月
# author: liuyu # author: liuyu
# date: 2020/3/6 # date: 2020/3/6
import json
import logging import logging
from urllib.parse import quote
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from django.http import HttpResponsePermanentRedirect, FileResponse, HttpResponse from django.http import HttpResponsePermanentRedirect, FileResponse, HttpResponse
@ -15,12 +17,13 @@ from api.models import Apps
from api.utils.modelutils import get_redirect_server_domain, add_remote_info_from_request, \ from api.utils.modelutils import get_redirect_server_domain, add_remote_info_from_request, \
get_app_download_uri get_app_download_uri
from api.utils.response import BaseResponse from api.utils.response import BaseResponse
from common.base.baseutils import get_real_ip_address, make_random_uuid, get_server_domain_from_request from common.base.baseutils import get_real_ip_address, make_random_uuid, get_server_domain_from_request, AesBaseCrypt
from common.cache.storage import TaskStateCache from common.cache.storage import TaskStateCache
from common.core.sysconfig import Config from common.core.sysconfig import Config
from common.core.throttle import ReceiveUdidThrottle1, ReceiveUdidThrottle2, VisitShortThrottle, InstallShortThrottle from common.core.throttle import ReceiveUdidThrottle1, ReceiveUdidThrottle2, VisitShortThrottle, InstallShortThrottle
from common.utils.caches import check_app_permission from common.utils.caches import check_app_permission
from common.utils.pending import get_pending_result from common.utils.pending import get_pending_result
from common.utils.token import verify_token, make_token
from fir_ser.celery import app from fir_ser.celery import app
from xsign.tasks import run_sign_task from xsign.tasks import run_sign_task
from xsign.utils.supersignutils import udid_bytes_to_dict, make_sign_udid_mobile_config from xsign.utils.supersignutils import udid_bytes_to_dict, make_sign_udid_mobile_config
@ -32,52 +35,55 @@ class IosUDIDView(APIView):
throttle_classes = [ReceiveUdidThrottle1, ReceiveUdidThrottle2] throttle_classes = [ReceiveUdidThrottle1, ReceiveUdidThrottle2]
def post(self, request, short): def post(self, request, short):
p_info = request.query_params.get('p')
p_token = app_id = ''
if p_info:
p_token = p_info[:52]
app_id = p_info[55:len(p_info) - 3]
if not p_token or not app_id:
return HttpResponsePermanentRedirect(Config.WEB_DOMAIN)
stream_f = str(request.body) stream_f = str(request.body)
format_udid_info = udid_bytes_to_dict(stream_f) format_udid_info = udid_bytes_to_dict(stream_f)
logger.info(f"short {short} receive new udid {format_udid_info}") logger.info(f"short {short} receive new udid {format_udid_info}")
server_domain = get_redirect_server_domain(request) server_domain = get_redirect_server_domain(request)
try: try:
app_obj = Apps.objects.filter(short=short).first() app_obj = Apps.objects.filter(short=short, app_id=app_id).first()
if app_obj: if app_obj:
server_domain = get_app_download_uri(request, app_obj.user_id, app_obj, preview=False) if p_token and verify_token(p_token, app_obj.app_id, True):
if app_obj.issupersign and app_obj.user_id.supersign_active: server_domain = get_app_download_uri(request, app_obj.user_id, app_obj, preview=False)
res = check_app_permission(app_obj, BaseResponse()) if app_obj.issupersign and app_obj.user_id.supersign_active:
if res.code != 1000: res = check_app_permission(app_obj, BaseResponse())
msg = "&msg=%s" % res.msg if res.code != 1000:
else: msg = "&msg=%s" % res.msg
client_ip = get_real_ip_address(request)
logger.info(f"client_ip {client_ip} short {short} app_info {app_obj}")
# from api.utils.app.supersignutils import IosUtils
# ios_obj = IosUtils(format_udid_info, app_obj.user_id, app_obj)
# ios_obj.sign_ipa(client_ip)
# return Response('ok')
c_task = run_sign_task.apply_async((format_udid_info, short, client_ip))
add_remote_info_from_request(request, f'{app_obj}-{format_udid_info}')
task_id = c_task.id
logger.info(f"sign app {app_obj} task_id:{task_id}")
try:
result = c_task.get(propagate=False, timeout=3)
except TimeoutError:
logger.error(f"get task task_id:{task_id} result timeout")
result = ''
if c_task.successful():
c_task.forget()
msg = "&msg=%s" % result
else: else:
msg = "&task_id=%s" % task_id client_ip = get_real_ip_address(request)
logger.info(f"client_ip {client_ip} short {short} app_info {app_obj}")
# from api.utils.app.supersignutils import IosUtils
# ios_obj = IosUtils(format_udid_info, app_obj.user_id, app_obj)
# ios_obj.sign_ipa(client_ip)
# return Response('ok')
data = {
'format_udid_info': format_udid_info,
'short': short,
'app_id': app_id,
'client_ip': client_ip,
'r_token': make_token(app_obj.app_id, time_limit=30, key='receive_udid', force_new=True)
}
encrypt_data = AesBaseCrypt().get_encrypt_uid(json.dumps(data))
msg = "&task_token=%s" % quote(encrypt_data, safe='/', encoding=None, errors=None)
else:
return HttpResponsePermanentRedirect(f"{server_domain}/{short}")
else: else:
return HttpResponsePermanentRedirect( return HttpResponsePermanentRedirect(f"{server_domain}/{short}")
"%s/%s" % (server_domain, short))
else: else:
return HttpResponsePermanentRedirect( return HttpResponsePermanentRedirect(f"{server_domain}/{short}")
"%s/%s" % (server_domain, short))
except Exception as e: except Exception as e:
msg = "&msg=系统内部错误" msg = "&msg=系统内部错误"
logger.error(f"short {short} receive udid Exception:{e}") logger.error(f"short {short} receive udid Exception:{e}")
return HttpResponsePermanentRedirect( return HttpResponsePermanentRedirect(f"{server_domain}/{short}?udid={format_udid_info.get('udid')}{msg}")
"%s/%s?udid=%s%s" % (server_domain, short, format_udid_info.get("udid"), msg))
def expect_func(result, *args, **kwargs): def expect_func(result, *args, **kwargs):
@ -131,6 +137,53 @@ class TaskView(APIView):
res.code = 1002 res.code = 1002
return Response(res.dict) return Response(res.dict)
def post(self, request, short):
res = BaseResponse()
task_token = request.data.get('task_token', None)
client_ip = get_real_ip_address(request)
if task_token:
data = json.loads(AesBaseCrypt().get_decrypt_uid(task_token))
if client_ip != data.get('client_ip', ''):
res.msg = '检测到网络异常,请重试'
res.code = 1001
if short != data.get('short', ''):
res.msg = '数据异常,请重试'
res.code = 1002
app_obj = Apps.objects.filter(short=short, app_id=data.get('app_id')).first()
if not app_obj:
res.msg = '错误,数据异常,请重试'
res.code = 1003
if not verify_token(data.get('r_token', ''), app_obj.app_id, True):
res.msg = '非法,数据异常,请重试'
res.code = 1004
if res.code != 1000:
return Response(res.dict)
format_udid_info = data.get('format_udid_info')
c_task = run_sign_task.apply_async((format_udid_info, short, client_ip))
add_remote_info_from_request(request, f'{app_obj}-{format_udid_info}')
task_id = c_task.id
logger.info(f"sign app {app_obj} task_id:{task_id}")
try:
result = c_task.get(propagate=False, timeout=3)
except TimeoutError:
logger.error(f"get task task_id:{task_id} result timeout")
result = ''
if c_task.successful():
c_task.forget()
res.result = result
else:
res.task_id = task_id
return Response(res.dict)
else:
res.code = 1001
res.msg = '数据异常,请重试'
return Response(res.dict)
class ShowUdidView(View): class ShowUdidView(View):
def get(self, request): def get(self, request):
@ -140,7 +193,7 @@ class ShowUdidView(View):
server_domain = get_server_domain_from_request(request, Config.POST_UDID_DOMAIN) server_domain = get_server_domain_from_request(request, Config.POST_UDID_DOMAIN)
path_info_lists = [server_domain, "show_udid"] path_info_lists = [server_domain, "show_udid"]
udid_url = "/".join(path_info_lists) udid_url = "/".join(path_info_lists)
ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, 'show_udid_info', 'flyapps.cn', '查询设备udid') ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, 'flyapps.cn', '查询设备udid')
response = FileResponse(ios_udid_mobile_config) response = FileResponse(ios_udid_mobile_config)
response['Content-Type'] = "application/x-apple-aspen-config" response['Content-Type'] = "application/x-apple-aspen-config"
response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileconfig' response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileconfig'

@ -23,7 +23,7 @@
签名时间:{{ now_time }} 签名时间:{{ now_time }}
</mj-text> </mj-text>
<mj-text color="#525252"> <mj-text color="#525252">
失败原因:开发者 {{developer_obj.issuer_id}} 状态 {developer_obj.get_status_display()} 失败原因:开发者 {{developer_obj.issuer_id}} 状态 {{developer_obj.get_status_display}}
</mj-text> </mj-text>
<mj-text color="#525252"> <mj-text color="#525252">
开发者备注:{{developer_obj.description}}。请登录后台查看具体失败信息 开发者备注:{{developer_obj.description}}。请登录后台查看具体失败信息

Loading…
Cancel
Save