增加应用下载授权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}
})
},
@ -599,15 +600,27 @@ export default {
window.location.href = this.mobileprovision;
},
check_msg() {
if (this.$route.query.udid) {
if (this.$route.query.task_id) {
this.wrong = true;
this.msg = '签名处理中,请耐心等待';
this.loop_check_task()
} else if (this.$route.query.msg) {
this.wrong = true;
this.show_err_msg(this.$route.query.msg);
}
if (this.$route.query.udid && this.$route.query.task_token) {
this.wrong = true;
this.msg = '签名处理中,请耐心等待';
gettask(data => {
if (data.code === 1000) {
if (data.task_id) {
this.$route.query.task_id = data.task_id;
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() {

@ -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}
})
},
@ -564,15 +565,27 @@ export default {
window.location.href = this.mobileprovision;
},
check_msg() {
if (this.$route.query.udid) {
if (this.$route.query.task_id) {
this.wrong = true;
this.msg = '签名处理中,请耐心等待';
this.loop_check_task()
} else if (this.$route.query.msg) {
this.wrong = true;
this.show_err_msg(this.$route.query.msg);
}
if (this.$route.query.udid && this.$route.query.task_token) {
this.wrong = true;
this.msg = '签名处理中,请耐心等待';
gettask(data => {
if (data.code === 1000) {
if (data.task_id) {
this.$route.query.task_id = data.task_id;
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() {

@ -9,14 +9,169 @@
<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"/>
</el-dialog>
<el-form label-width="80px">
<el-form-item label="访问密码" label-width="200px">
<el-dialog
: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">
<div slot="content">
{{ passwordtip.msg }}<br>
<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>
<el-switch
@ -29,7 +184,12 @@
</el-switch>
</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>
@ -93,9 +253,10 @@
</template>
<script>
import {apputils,} from "@/restful"
import {appDownloadToken, apputils,} from "@/restful"
import {deepCopy} from "@/utils";
import BindDomain from "@/components/base/BindDomain";
import {format_time} from "@/utils/base/utils";
export default {
name: "FirAppInfossecurity",
@ -115,9 +276,124 @@ export default {
wxeasy_disable: false,
defualt_dtitle: '专属下载页域名',
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: {
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() {
this.passwordflag = false;
this.showdownloadflag = false;
@ -144,7 +420,7 @@ export default {
});
},
setbuttondefaltpass(currentapp) {
if (currentapp.password === '') {
if (currentapp.need_password === false) {
this.passwordtip.val = 'off';
this.showpasswordevent("off");
} else {
@ -191,57 +467,6 @@ export default {
this.setxeasytypeshow(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) {
if (newval === "on") {
if (this.showdownloadflag) {
@ -264,19 +489,21 @@ export default {
showpasswordevent(newval) {
if (newval === "on") {
if (this.passwordflag) {
this.setaccesspassword()
} else {
this.passwordtip.msg = '访问密码:' + this.currentapp.password;
this.saveappinfo({
"need_password": 1,
});
this.currentapp.need_password = 1;
}
this.passwordtip.msg = '已经开启授权码下载功能'
} else {
if (this.passwordflag) {
this.saveappinfo({
"password": '',
"need_password": 0,
});
this.currentapp.need_password = 0;
}
this.currentapp.password = '';
this.$store.dispatch('doucurrentapp', this.currentapp);
this.passwordtip.msg = '无访问密码'
this.passwordtip.msg = '未开启授权码下载功能'
}
},
@ -342,6 +569,9 @@ export default {
'$store.state.currentapp': function () {
this.appinit();
},
'makeTokenInfo.token': function () {
this.tokendisable = !!(this.makeTokenInfo.token && this.makeTokenInfo.token.length > 0);
}
}, computed: {}
}
</script>

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

@ -35,23 +35,6 @@ function ErrorMsg(error) {
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") {
Axios
.put(url, params)
@ -74,7 +57,7 @@ function getData(methods = 'GET', url, params = {}, callBack) {
});
} else
Axios
.get(url, params)
.get(url, {params: params})
.then(function (response) {
callBack(response.data);
let x = '';
@ -165,7 +148,7 @@ let SIGNSEVER = DOMAIN + '/api/v1/fir/xsign';
export function gettask(callBack, params) {
getData(
'GET',
params.methods,
SIGNSEVER + '/task/' + params.short,
params.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) {
@ -951,7 +966,7 @@ export function signoperatemessage(callBack, params, load = true) {
/**获取签名任务状态 */
export function gettask(callBack, params, load = true) {
getData(
'GET',
params.methods,
SIGNSEVER + '/task/' + params.short,
params.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)
created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
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)
issupersign = 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):
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 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.download import ShortDownloadView, InstallView, DownloadView
from api.views.getip import GetRemoteIp
@ -48,6 +48,7 @@ urlpatterns = [
re_path("^storage$", StorageView.as_view()),
re_path("^storage/clean$", CleanStorageView.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("^upload$", UploadView.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 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, \
get_origin_domain_name
@ -228,3 +228,15 @@ def get_wx_nickname(openid):
if obj:
nickname = obj.nickname
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):
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()
def get_has_combo(self, obj):
@ -508,3 +501,9 @@ class NotifyConfigSerializer(serializers.ModelSerializer):
def get_senders(self, obj):
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月
# author: liuyu
# date: 2020/3/4
import copy
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.response import Response
from rest_framework.views import APIView
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.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.utils import delete_local_files, delete_app_screenshots_files
from common.cache.state import MigrateStorageState
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.download import get_app_download_url
from common.utils.storage import Storage
from common.utils.token import verify_token, get_random_download_token
logger = logging.getLogger(__name__)
@ -172,9 +176,9 @@ class AppInfoView(APIView):
app_obj.description = data.get("description", app_obj.description)
app_obj.short = data.get("short", app_obj.short)
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)
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):
app_obj.wxeasytype = data.get("wxeasytype", app_obj.wxeasytype)
else:
@ -313,6 +317,23 @@ class AppReleaseInfoView(APIView):
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):
authentication_classes = [ExpiringTokenAuthentication, ]
@ -330,3 +351,80 @@ class AppsQrcodeShowView(APIView):
res.data = app_serializer.data
res.has_next = page_obj.page.has_next()
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)
short = query_params.get("short", None)
release_id = query_params.get("release_id", None)
isdownload = query_params.get("isdownload", None)
password = query_params.get("password", None)
udid = query_params.get("udid", None)
@ -179,7 +178,7 @@ class InstallView(APIView):
return Response(res.dict)
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:
res.code = 1004
res.msg = "token校验失败"

@ -18,6 +18,7 @@ from api.utils.response import BaseResponse
from api.utils.serializer import NotifyReceiverSerializer, NotifyConfigSerializer
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.cache.storage import NotifyLoopCache
from common.core.auth import ExpiringTokenAuthentication
from common.core.sysconfig import Config
from common.utils.caches import login_auth_failed
@ -74,6 +75,8 @@ class NotifyConfigView(APIView):
request.user.notify_available_signs = notify_available_signs
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'])
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',
'notify_available_signs').first()
@ -105,6 +108,12 @@ class NotifyConfigView(APIView):
notify_config_obj.enable_email = enable_email
if enable_weixin is not None or enable_email is not None:
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)

@ -276,15 +276,6 @@ def get_format_time():
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):
if request.META.get('HTTP_X_FORWARDED_FOR', None):
return request.META.get('HTTP_X_FORWARDED_FOR')

@ -5,6 +5,7 @@
# date: 2022/1/13
import logging
from rest_framework.exceptions import Throttled
from rest_framework.views import exception_handler
from common.core.response import ApiResponse
@ -17,6 +18,15 @@ def common_exception_handler(exc, context):
ret = exception_handler(exc, context) # 是Response对象,它内部有个data
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 的,我们自己来处理
return ApiResponse(msg='error', result=str(exc), code=5000)
else:

@ -226,8 +226,7 @@
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;">
失败原因:开发者 {{ developer_obj.issuer_id }} 状态
{developer_obj.get_status_display()}
</div>
{{ developer_obj.get_status_display }}</div>
</td>
</tr>
<tr>

@ -9,9 +9,9 @@ import os
import time
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 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.core.sysconfig import Config
from common.utils.caches import consume_user_download_times
@ -20,9 +20,18 @@ from common.utils.storage import Storage, LocalStorage
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()
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)
download_url_type = 'plist'
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)
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:
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()
if app_info:
app_info['d_count'] = get_app_d_count_by_app_id(app_id)
app_password = app_info.get("password")
if not check_app_password(app_password, password):
return None
return app_info
need_password = app_info.get("need_password")
if need_password and not is_download:
if not check_app_access_token(app_id, password):
return False, '下载授权码有误'
return True, app_info
app_instance_cache = AppInstanceCache(app_id)
app_obj_cache = app_instance_cache.get_storage_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',
'user_id__certification__status').first()
if app_obj_cache:
app_obj_cache['d_count'] = get_app_d_count_by_app_id(app_id)
app_instance_cache.set_storage_cache(app_obj_cache, limit)
if not app_obj_cache:
return None
app_password = app_obj_cache.get("password")
if not check_app_password(app_password, password):
return None
return False, '应用不存在'
need_password = app_obj_cache.get("need_password")
if need_password and not is_download:
if not check_app_access_token(app_id, password):
return False, '下载授权码有误'
return app_obj_cache
return True, app_obj_cache
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
def get_app_download_url(request, res, app_id, short, password, release_id, isdownload, udid):
app_obj = get_app_instance_by_cache(app_id, password, 900, udid)
if app_obj:
def get_app_download_url(request, res, app_id, short, password, release_id, is_download, udid):
status, app_obj = get_app_instance_by_cache(app_id, password, is_download, 900, udid)
if status:
if app_obj.get("type") == 0:
app_type = '.apk'
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600)
else:
app_type = '.ipa'
if isdownload:
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, udid=udid)
else:
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, isdownload,
udid=udid)
download_url, extra_url = get_download_url_by_cache(app_obj, release_id + app_type, 600, is_download,
udid=udid)
res.data = {"download_url": download_url, "extra_url": extra_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
res.code = 1006
res.msg = "该应用不存在"
res.msg = str(app_obj)
return res

@ -72,3 +72,24 @@ def generate_numeric_token_of_length(length, random_str=''):
def generate_alphanumeric_token_of_length(length):
return "".join(
[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)
@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 ssl_pem_path: pem证书的绝对路径
:param ssl_key_path: key证书的绝对路径
@ -92,8 +94,14 @@ class ResignApp(object):
from cryptography.hazmat.primitives.serialization import pkcs7
from cryptography import x509
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-----',
open(ssl_pem_path, 'r').read(), re.S)
ssl_pem_data.decode('utf-8'), re.S)
if len(cert_list) == 0:
raise Exception('load cert failed')
else:
@ -101,7 +109,7 @@ class ResignApp(object):
cas = [cert]
if len(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(
data=sign_data.encode('utf-8'),
signers=[

@ -330,11 +330,6 @@ class AdminBillInfoSerializer(BillInfoSerializer):
read_only_fields = ["id", "user_id", "to_user_id", "action", "number", "app_info", "udid",
"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 Meta:

@ -9,6 +9,7 @@ import os
import time
import uuid
import zipfile
from io import BytesIO
import xmltodict
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.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, \
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.cache.state import CleanErrorBundleIdSignDataState
from common.cache.storage import RedisCacheBase
from common.constants import DeviceStatus, AppleDeveloperStatus, SignStatus
from common.core.sysconfig import Config
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
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"):
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)
if ssl_key_path and ssl_pem_path and os.path.isfile(ssl_key_path) and os.path.isfile(ssl_pem_path):
mobile_config_tmp_dir = os.path.join(SUPER_SIGN_ROOT, 'tmp', 'mobile_config')
if not os.path.exists(mobile_config_tmp_dir):
os.makedirs(mobile_config_tmp_dir)
sign_mobile_config_path = os.path.join(mobile_config_tmp_dir, 'sign_' + short)
logger.info(f"make sing mobile config {sign_mobile_config_path}")
if os.path.isfile(sign_mobile_config_path):
return open(sign_mobile_config_path, 'rb')
status, result = ResignApp.sign_mobile_config(
make_udid_mobile_config(udid_url, bundle_id, app_name),
ssl_pem_path,
ssl_key_path)
ssl_key_cache = RedisCacheBase(AesBaseCrypt().get_encrypt_uid(ssl_key_path))
ssl_pem_cache = RedisCacheBase(AesBaseCrypt().get_encrypt_uid(ssl_pem_path))
ssl_key_data = ssl_key_cache.get_storage_cache()
ssl_pem_data = ssl_pem_cache.get_storage_cache()
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):
ssl_key_cache.set_storage_cache(open(ssl_key_path, 'rb').read(), 24 * 3600)
ssl_pem_cache.set_storage_cache(open(ssl_pem_path, 'rb').read(), 24 * 3600)
ssl_key_data = ssl_key_cache.get_storage_cache()
ssl_pem_data = ssl_pem_cache.get_storage_cache()
if ssl_key_data and ssl_pem_data:
status, result = ResignApp.sign_mobile_config(make_udid_mobile_config(udid_url, bundle_id, app_name),
ssl_pem_path, ssl_key_path, ssl_pem_data, ssl_key_data)
if status and result.get('data'):
with open(sign_mobile_config_path, 'wb') as f:
f.write(result.get('data'))
return open(sign_mobile_config_path, 'rb')
buffer = BytesIO(result.get("data"))
return buffer
else:
logger.error(
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.app_obj = app_obj
self.user_obj = user_obj
self.get_developer_auth()
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)
@ -537,7 +535,7 @@ class IosUtils(object):
res = check_app_permission(self.app_obj, BaseResponse())
if res.code != 1000:
return False, {'code': res.code, 'msg': res.msg}
self.get_developer_auth(True)
if not self.developer_obj:
msg = "udid %s app %s not exists apple developer" % (self.udid, self.app_obj)
d_result['code'] = 1005

@ -6,6 +6,7 @@
import logging
import os
import random
from django.urls import reverse
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.sysconfig import Config
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 xsign.models import APPToDeveloper, APPSuperSignUsedInfo
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__)
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)
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):
@ -69,10 +72,8 @@ class XsignDownloadView(APIView):
release_obj = AppReleaseInfo.objects.filter(release_id=filename.split('.')[0]).first()
if release_obj:
app_obj = release_obj.app_id
udid_url = get_post_udid_url(request, app_obj.short)
ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, f'{app_obj.app_id}_{app_obj.short}',
app_obj.bundle_id,
app_obj.name)
udid_url = get_post_udid_url(request, app_obj)
ios_udid_mobile_config = make_sign_udid_mobile_config(udid_url, app_obj.bundle_id, app_obj.name)
return file_response(ios_udid_mobile_config, make_random_uuid() + '.mobileconfig',
"application/x-apple-aspen-config")
return ApiResponse(code=1004, msg="mobile_config release_id error")

@ -3,7 +3,9 @@
# project: 3月
# author: liuyu
# date: 2020/3/6
import json
import logging
from urllib.parse import quote
from celery.exceptions import TimeoutError
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, \
get_app_download_uri
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.core.sysconfig import Config
from common.core.throttle import ReceiveUdidThrottle1, ReceiveUdidThrottle2, VisitShortThrottle, InstallShortThrottle
from common.utils.caches import check_app_permission
from common.utils.pending import get_pending_result
from common.utils.token import verify_token, make_token
from fir_ser.celery import app
from xsign.tasks import run_sign_task
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]
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)
format_udid_info = udid_bytes_to_dict(stream_f)
logger.info(f"short {short} receive new udid {format_udid_info}")
server_domain = get_redirect_server_domain(request)
try:
app_obj = Apps.objects.filter(short=short).first()
app_obj = Apps.objects.filter(short=short, app_id=app_id).first()
if app_obj:
server_domain = get_app_download_uri(request, app_obj.user_id, app_obj, preview=False)
if app_obj.issupersign and app_obj.user_id.supersign_active:
res = check_app_permission(app_obj, BaseResponse())
if res.code != 1000:
msg = "&msg=%s" % res.msg
else:
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
if p_token and verify_token(p_token, app_obj.app_id, True):
server_domain = get_app_download_uri(request, app_obj.user_id, app_obj, preview=False)
if app_obj.issupersign and app_obj.user_id.supersign_active:
res = check_app_permission(app_obj, BaseResponse())
if res.code != 1000:
msg = "&msg=%s" % res.msg
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:
return HttpResponsePermanentRedirect(
"%s/%s" % (server_domain, short))
return HttpResponsePermanentRedirect(f"{server_domain}/{short}")
else:
return HttpResponsePermanentRedirect(
"%s/%s" % (server_domain, short))
return HttpResponsePermanentRedirect(f"{server_domain}/{short}")
except Exception as e:
msg = "&msg=系统内部错误"
logger.error(f"short {short} receive udid Exception:{e}")
return HttpResponsePermanentRedirect(
"%s/%s?udid=%s%s" % (server_domain, short, format_udid_info.get("udid"), msg))
return HttpResponsePermanentRedirect(f"{server_domain}/{short}?udid={format_udid_info.get('udid')}{msg}")
def expect_func(result, *args, **kwargs):
@ -131,6 +137,53 @@ class TaskView(APIView):
res.code = 1002
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):
def get(self, request):
@ -140,7 +193,7 @@ class ShowUdidView(View):
server_domain = get_server_domain_from_request(request, Config.POST_UDID_DOMAIN)
path_info_lists = [server_domain, "show_udid"]
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['Content-Type'] = "application/x-apple-aspen-config"
response['Content-Disposition'] = 'attachment; filename=' + make_random_uuid() + '.mobileconfig'

@ -23,7 +23,7 @@
签名时间:{{ now_time }}
</mj-text>
<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 color="#525252">
开发者备注:{{developer_obj.description}}。请登录后台查看具体失败信息

Loading…
Cancel
Save