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 %}
+
+
+
+
+
+
+- 首页
+- 分享
+
+{% if u.is_admin %}
+- 管理后台
+
+{% endif %}
+
+
+
+
+
+{% 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次。链接一旦被(非分享人的登录用户)访问,分享生效,后续访问链接失效。
+
+
+
+
+
+
+
+
+
+
+
+ | 链接地址 |
+ 创建时间 |
+ ID列表 |
+
+
+
+
+ {% for one in sharelinks %}
+
+ | {{one.shash}} |
+ {{one.create_time}} |
+ {{one.peers}} |
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+{% 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
-
-
-
-
-
-
-
-
-
-
-
- - 首页
-
-
-
-
-
- {% if u.is_admin %}
- - 管理后台
-
- {% endif %}
-
-
-
-
-
+{% 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
zcmeI5du$uYeaE?ciqDlT+dQ9NE6TF8mL>5alF}V6jn3wsRm+ks%Sd#^VX-7v)Y^P`
zm-5*)@&|R!T@auTBte@XXp;g>Qy^{71O
C9v1^mlc&ikDvTr&i68>K37opYQdxmZV-sk^;
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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+itoZU38sewx07Dkf6@$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