From 66a65b7923e95fb6effbd1f36782c00dfebac249 Mon Sep 17 00:00:00 2001 From: kingmo888 <17401091+kingmo888@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:11:19 +0800 Subject: [PATCH] =?UTF-8?q?V1.2=20=E5=A2=9E=E5=8A=A0=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=88=86=E4=BA=AB=E9=93=BE=E6=8E=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- api/admin_user.py | 1 + api/migrations/0001_initial.py | 30 +++++++++- api/models_work.py | 26 +++++++- api/templates/base.html | 44 ++++++++++++++ api/templates/msg.html | 12 ++++ api/templates/share.html | 105 +++++++++++++++++++++++++++++++++ api/templates/show_work.html | 50 ++-------------- api/urls.py | 1 + api/views_api.py | 12 +--- api/views_front.py | 102 +++++++++++++++++++++++++++++++- db.sqlite3_bak | Bin 0 -> 155648 bytes db/db.sqlite3 | Bin 147456 -> 155648 bytes 13 files changed, 325 insertions(+), 62 deletions(-) create mode 100644 api/templates/base.html create mode 100644 api/templates/msg.html create mode 100644 api/templates/share.html create mode 100644 db.sqlite3_bak diff --git a/README.md b/README.md index 37ee8c5..3abc4fb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

一个 python 实现的 Rustdesk API 接口,支持 WebUI 管理
- +
@@ -137,7 +137,7 @@ services: ## 开发计划 -- [ ] 分享设备给其他已注册用户 +- [-] 分享设备给其他已注册用户 > 说明:类似网盘url分享,url激活后可以获得某个或某组或某个标签下的设备 > 备注:其实web api作为中间件,可做的不多,更多功能还是需要修改客户端来实现,就不太值当了。 diff --git a/api/admin_user.py b/api/admin_user.py index fbfaeab..898a04d 100644 --- a/api/admin_user.py +++ b/api/admin_user.py @@ -93,6 +93,7 @@ admin.site.register(models.RustDeskToken, models.RustDeskTokenAdmin) admin.site.register(models.RustDeskTag, models.RustDeskTagAdmin) admin.site.register(models.RustDeskPeer, models.RustDeskPeerAdmin) admin.site.register(models.RustDesDevice, models.RustDesDeviceAdmin) +admin.site.register(models.ShareLink, models.ShareLinkAdmin) admin.site.unregister(Group) admin.site.site_header = 'RustDesk自建Web' admin.site.site_title = '未定义' \ No newline at end of file diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index d647e1e..6ead33e 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2023-12-04 21:05 +# Generated by Django 4.2.7 on 2023-12-14 12:08 from django.db import migrations, models @@ -138,6 +138,34 @@ class Migration(migrations.Migration): "ordering": ("-username",), }, ), + migrations.CreateModel( + name="ShareLink", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uid", models.CharField(max_length=16, verbose_name="用户ID")), + ("shash", models.CharField(max_length=60, verbose_name="链接Key")), + ("peers", models.CharField(max_length=20, verbose_name="机器ID列表")), + ("is_used", models.BooleanField(default=False, verbose_name="是否使用")), + ("is_expired", models.BooleanField(default=False, verbose_name="是否过期")), + ( + "create_time", + models.DateTimeField(auto_now_add=True, verbose_name="生成时间"), + ), + ], + options={ + "verbose_name": "分享链接", + "verbose_name_plural": "链接列表", + "ordering": ("-create_time",), + }, + ), migrations.CreateModel( name="UserProfile", fields=[ diff --git a/api/models_work.py b/api/models_work.py index 9ec1fa2..0d3968e 100644 --- a/api/models_work.py +++ b/api/models_work.py @@ -87,4 +87,28 @@ class RustDesDeviceAdmin(admin.ModelAdmin): list_display = ('rid', 'hostname', 'memory', 'uuid', 'version', 'create_time', 'update_time') search_fields = ('hostname', 'memory') list_filter = ('rid', ) - \ No newline at end of file + + + +class ShareLink(models.Model): + ''' 分享链接 + ''' + uid = models.CharField(verbose_name='用户ID', max_length=16) + shash = models.CharField(verbose_name='链接Key', max_length=60) + peers = models.CharField(verbose_name='机器ID列表', max_length=20) + is_used = models.BooleanField(verbose_name='是否使用', default=False) + is_expired = models.BooleanField(verbose_name='是否过期', default=False) + create_time = models.DateTimeField(verbose_name='生成时间', auto_now_add=True) + + + + class Meta: + ordering = ('-create_time',) + verbose_name = "分享链接" + verbose_name_plural = "链接列表" + + +class ShareLinkAdmin(admin.ModelAdmin): + list_display = ('shash', 'uid', 'peers', 'is_used', 'is_expired', 'create_time') + search_fields = ('peers', ) + list_filter = ('is_used', 'uid', 'is_expired' ) \ No newline at end of file diff --git a/api/templates/base.html b/api/templates/base.html new file mode 100644 index 0000000..0bc6073 --- /dev/null +++ b/api/templates/base.html @@ -0,0 +1,44 @@ +{% load static %} + + + +{% block title %}{% endblock %} + + + + +{% block link %}{% endblock %} + + + + + +

+ +
+{% block legend_name %}{% endblock %} +
+ +{% block content %}{% endblock %} + + + \ No newline at end of file diff --git a/api/templates/msg.html b/api/templates/msg.html new file mode 100644 index 0000000..86ff15c --- /dev/null +++ b/api/templates/msg.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %}{{title}}{% endblock %} +{% block legend_name %}信息{% endblock %} +{% block content %} +
+
+{% autoescape off %} + {{msg}} + {% endautoescape %} +
+ +{% endblock %} \ No newline at end of file diff --git a/api/templates/share.html b/api/templates/share.html new file mode 100644 index 0000000..29cb3dc --- /dev/null +++ b/api/templates/share.html @@ -0,0 +1,105 @@ + +{% extends "base.html" %}{% load static %} +{% block title %}分享机器{% endblock %} +{% block link %}{% endblock %} +{% block legend_name %}分享机器给其他用户{% endblock %} +{% block content %} + + +
+
+
请将要分享的机器调整到右侧
+
+ +
+
1、链接有效期为15分钟,切勿随意分享给他人。
+
2、所分享的机器,被分享人享有相同的权限,如果机器设置了保存密码,被分享人也可以直接连接。
+
3、为保障安全,链接有效期为15分钟、链接仅有效1次。链接一旦被(非分享人的登录用户)访问,分享生效,后续访问链接失效。
+ +
+ + + + + + + + + + + + + + + + + {% for one in sharelinks %} + + + + + + + {% endfor %} + +
链接地址创建时间ID列表
{{one.shash}} {{one.create_time}} {{one.peers}}
+
+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/api/templates/show_work.html b/api/templates/show_work.html index 4ca8970..992480f 100644 --- a/api/templates/show_work.html +++ b/api/templates/show_work.html @@ -1,46 +1,7 @@ -{% load static %} - - - - RustDesk WebUI - - - - - - - - - - - - -
- 综合屏 -
- +{% extends "base.html" %} +{% block title %}RustDesk WebUI{% endblock %} +{% block legend_name %}综合屏{% endblock %} +{% block content %}
@@ -154,5 +115,4 @@ layui.use('element', function(){
- - \ No newline at end of file +{% endblock %} \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index 28cd52f..e28f707 100644 --- a/api/urls.py +++ b/api/urls.py @@ -18,4 +18,5 @@ urlpatterns = [ #url(r'^register',views.register), url(r'^user_action',views.user_action), # 前端 url(r'^work',views.work), # 前端 + url(r'^share',views.share), # 前端 ] diff --git a/api/views_api.py b/api/views_api.py index 5dc381e..2c4dc86 100644 --- a/api/views_api.py +++ b/api/views_api.py @@ -11,17 +11,7 @@ from django.db.models import Q import copy from .views_front import * -salt = 'xiaomo' -EFFECTIVE_SECONDS = 7200 -def getStrMd5(s): - if not isinstance(s, (str,)): - s = str(s) - - myHash = hashlib.md5() - myHash.update(s.encode()) - - return myHash.hexdigest() def login(request): result = {} @@ -45,7 +35,7 @@ def login(request): user.rid = rid user.uuid = uuid user.autoLogin = autoLogin - user.rtype = rtype + user.rtype = rtype user.deviceInfo = json.dumps(deviceInfo) user.save() diff --git a/api/views_front.py b/api/views_front.py index 8a4ddf7..3f48324 100644 --- a/api/views_front.py +++ b/api/views_front.py @@ -6,7 +6,7 @@ from django.http import JsonResponse from django.db.models import Q from django.contrib.auth.decorators import login_required from django.contrib import auth -from api.models import RustDeskPeer, RustDesDevice, UserProfile +from api.models import RustDeskPeer, RustDesDevice, UserProfile, ShareLink from django.forms.models import model_to_dict from itertools import chain @@ -14,7 +14,20 @@ from django.db.models.fields import DateTimeField, DateField, CharField, TextFie import datetime from django.db.models import Model import json +import time +import hashlib +salt = 'xiaomo' +EFFECTIVE_SECONDS = 7200 + +def getStrMd5(s): + if not isinstance(s, (str,)): + s = str(s) + + myHash = hashlib.md5() + myHash.update(s.encode()) + + return myHash.hexdigest() def model_to_dict2(instance, fields=None, exclude=None, replace=None, default=None): """ @@ -87,7 +100,8 @@ def model_to_dict2(instance, fields=None, exclude=None, replace=None, default=No def index(request): - #return render(request, 'login3.html', {'info':''}) + if request.user and request.user.username!='AnonymousUser': + return HttpResponseRedirect('/api/work') return HttpResponseRedirect('/api/user_action?action=login') @@ -199,3 +213,87 @@ def work(request): print(all_info) return render(request, 'show_work.html', {'single_info':single_info, 'all_info':all_info, 'u':u}) + + +def check_sharelink_expired(sharelink): + now = datetime.datetime.now() + if sharelink.create_time > now: + return False + if (now - sharelink.create_time).seconds <15 * 60: + return False + else: + sharelink.is_expired = True + sharelink.save() + return True + + +@login_required(login_url='/api/user_action?action=login') +def share(request): + peers = RustDeskPeer.objects.filter(Q(uid=request.user.id)) + sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False)) + + + # 省资源:处理已过期请求,不主动定时任务轮询请求,在任意地方请求时,检查是否过期,过期则保存。 + now = datetime.datetime.now() + for sl in sharelinks: + check_sharelink_expired(sl) + sharelinks = ShareLink.objects.filter(Q(uid=request.user.id) & Q(is_used=False) & Q(is_expired=False)) + peers = [{'id':ix+1, 'name':f'{p.rid}|{p.alias}'} for ix, p in enumerate(peers)] + sharelinks = [{'shash':s.shash, 'is_used':s.is_used, 'is_expired':s.is_expired, 'create_time':s.create_time, 'peers':s.peers} for ix, s in enumerate(sharelinks)] + + if request.method == 'GET': + url = request.build_absolute_uri() + if url.endswith('share'): + return render(request, 'share.html', {'peers':peers, 'sharelinks':sharelinks}) + else: + shash = url.split('/')[-1] + sharelink = ShareLink.objects.filter(Q(shash=shash)) + msg = '' + title = '成功' + if not sharelink: + title = '错误' + msg = f'链接{url}:
分享链接不存在或已失效。' + else: + sharelink = sharelink[0] + if str(request.user.id) == str(sharelink.uid): + title = '错误' + msg = f'链接{url}:

咱就说,你不能把链接分享给自己吧?!' + else: + sharelink.is_used = True + sharelink.save() + peers = sharelink.peers + peers = peers.split(',') + # 自己的peers若重叠,需要跳过 + peers_self_ids = [x.rid for x in RustDeskPeer.objects.filter(Q(uid=request.user.id))] + peers_share = RustDeskPeer.objects.filter(rid__in=peers) + peers_share_ids = [x.rid for x in peers_share] + + for peer in peers_share: + if peer.rid in peers_self_ids: + continue + peer = RustDeskPeer.objects.get(rid=peer.rid) + peer.id = None + peer.uid = request.user.id + peer.save() + msg += f"{peer.rid}," + + msg += '已被成功获取。' + + return render(request, 'msg.html', {'title':msg, 'msg':msg}) + else: + data = request.POST.get('data', '[]') + + data = json.loads(data) + if not data: + return JsonResponse({'code':0, 'msg':'数据为空。'}) + rustdesk_ids = [x['title'].split('|')[0] for x in data] + rustdesk_ids = ','.join(rustdesk_ids) + sharelink = ShareLink( + uid=request.user.id, + shash = getStrMd5(str(time.time())+salt), + peers=rustdesk_ids, + ) + sharelink.save() + + return JsonResponse({'code':1, 'shash':sharelink.shash}) + diff --git a/db.sqlite3_bak b/db.sqlite3_bak new file mode 100644 index 0000000000000000000000000000000000000000..8968aa74807a49a85d5a9e5e1d3e007155eed0db GIT binary patch literal 155648 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 J{}J2&|9=jHepmni literal 0 HcmV?d00001 diff --git a/db/db.sqlite3 b/db/db.sqlite3 index 451e9954cb45292202a7c4f48cc340b4d02b7bde..8968aa74807a49a85d5a9e5e1d3e007155eed0db 100644 GIT binary patch delta 2415 zcmb_cdu&r>6u+mv-EDijeuX^>Y;AA%DqYw1KKdNPZEQ0F0|x8d+{02>frhdz>rmn( z+i)5r^70Zt4T%zrkHm}Rn*xS z**>yGZYy=>lqLuw&wJ(lrF;&|d%fO(S3XlJWx;|gC`mqEl8a6L95^8Pr2sF7O}=@z zs})>+(eIJuS50*}s88|{^oB{yN#XSoRFwVZT-X!fMcyZmnzPFjTvAY!Bwl{cEajvY zgQ7>2!IC|F!6kSlpFb$KS}Zv?@?O8h3-V4&APeSQeow#`@XH@sY&oz`6#Rm8f_n~u zTgL89R(2qf+5{GvDfA2#$@EB81%W}v|e?8f+DA{riv zB(}O2&>#?COJrbk1_>udA|u-qE<}R`*hgZ~N7F=SF%8by=k1B;@X+w!rdT{$lZo$- z#-k(AaF)Z7llYcnjD%Dd(cl0!(%+xWw-?f22W|SkxvmzcFV)UwpMz}~c(&*{xT=5# z8)#FB=dj9j8Jcw3;q8g!h1Lq?A{Ln_^aDDJK1Oe%=g}^-4LyXKkbq2NNdM@o=Cd?a zP)^2st87xWK}#@8_Q;b>;ZwZQt-m5O2RhaIzx2Q8uj{Xn3{UFqDkT8xPFq*nI@7i$ zZ5?S_owmusm##G^-Rd5hxv-+huK8VaT607*s%g_ys{dAhp?*=_lN1a;GwMdSiUR8( zD;LewF)nfzma}r#jKDa_*|LO{56oDZ8gh0zSo!T4j&YyljM>TA zVZU?yO_2eP437K$(Ce3WPumw0lHGrY8Yxa{klc5>pZfOQH)iCMm-fBlv=ORv$z2+y z$Qj*o>77*>{+;5b_!%sRZ>uD9n>Sa~Tg7JAkz9BASj)3Rv=S+P#Y_WK%zk6deZB!~sOl=ERUD>JdHy$=kC+cMd-4+WZ?s0a zTyZ#G$Ex5O^ii9EBLVfw312ln-wlhQ4AVWZH&1&NM%UQ!>MmG--|7J~T`^&>iEg?|jn<3Z@COp;wt*~(VlMWK3E@h0tAWR1F!7eDoYdV3! zC7n==Pj`@nZJqF|@`+`O@SZNHCLjqhO@Gz}kAY%o30~0!b@v;0l`mwBHXBT;=B70F zDy2p?_fXKZuB9>5!i5@_wYG3&IX*l^MkDW2*j0p{*dl+eC}wK7vY}{nSdOa}*<*>+ zi{D?yZ5SMkbF0^cxYZr4t%N}w(H%pv;afiW8aNhGl;S5M++=}`F;439sxxv<(l%`D zBR!W-H}}C=fO2dO!$#638KfB>3B$VM=S>R8Pr}qcmZRf_6XUcYNl*RhK03u8o#JMZ z`II5O9c9E?$)976lh^Gqqa@ei2fM*X^6u@1m86^eY1{{sKvR^m1)uAII{ZvGk-^*x zD+#7Jfw%QSb4G4Umc+!BDU`#c(02Ui!(g61)CWFdtq$pUUdoB^+v{KxXp^vBX(-mH zYbpKN{OhR=)^=+?)acb`)U~8VPOY3MtihvgWJN3SJ8ke_-V#Ti@s1_VU32AsS!yTT KOPK$u#eV^C2cCcc delta 1560 zcmah}TWlLe6y3Xiti84~PUE~|uid83V{3bN?e)fK9!>0qJVk^AP58jI5|>&{ov2Pq zM5&Urm8c+yq!SpBphZPgd_Wr2Na2G@^oysE`a>v1h%ZP$kV;7K(Mn~UCW?)aSgl5P z&pqe9c4lTjHP6nP7X(`^v-<=V547-r?&pv)be|MS;N+H=C5CCOBJ(}aVsKE8X?0D4 zT4bPRX~hkD)UrnkxfQutQk!MXFUzW=X!yzMNFE&aM}n#~9JwYDksr z2Bv&}M`HA3DlJKp98aZFlc~g*A}K+itoZU38s�ewx07Dkf6@$5*u=<#!hun9?I1 ziK)rsc(BG-P+CxnsG5vlt&s{~rAbva1>dd-7Qk{2mT;L5GhDn|t?G}^ zzBPQ!*;0toA!+zer;-;!^*05jTnNoo2x}oZA_egoSAKb_KO73?;G3>sL1&qDmik$AQWT_hACOWXN9H$SPq3FQV4%0)D^%|4pwfsUq>Kf5;!LC=?qUd0gogG z{ejM-G$IwqyI4Y^cTdD^U_{8XFK0kZAKd7tVQGh|eqdVwJ z^dWi!okJ(lFp8o8ExrUf_V9Y9WFOs#JK<=wY&3pL(uqiixrHN?_Fhr3K?lH#TWjOi znsr>ccSt9iZJ7J1*XH9{^O||Ze9=5;wq<4eb(5(>G%(;9=kdsT3)e_(xrfKs)@81N z+QLB||G4hq>Z$D$d2HNpb9FgRfXBX#TF$#|4{g-e?x(ic@bBKDiL{%x>;7fw{hu$c zW}o=U)fLs@!!TB5yxqXQmm`i>7YlUxpX- z^SZBfF8CV|srn4F-&sjMe+(L-giw!j)!q%y18@0;nJ*6nE|hr5 zM;aT3z2rBI?bFSL8%XgmZN@{IhuK$*2OEv$k2jEo9;hIjL#)fXkB-)qo9~>xo*eCf zR?^f-8@<^9DqF{qXPVgkcR}wZram|Tdso#yxC1mB8h{Zp)W%Ar tWdO>^$E|DxY+1PXP`J7xZN2VG6BdJ;W)iZ+M>kRS;f%qa)vr7d{sAlEh?D>T