Compare commits

...

6 Commits

Author SHA1 Message Date
lejianwen
9c794e9d4b fix(build): Fix no admin in deb (#119 #120) 2025-02-03 13:29:35 +08:00
lejianwen
01f697d279 fix(api): Add Default Token Expire (#113) 2025-02-03 13:18:08 +08:00
lejianwen
6cdc37333b style: webclient 2025-02-03 00:01:10 +08:00
Tao Chen
ae32915565 feat(ldap): Add LDAP
* rename: Admin to AdminGroup

* update

* cleanup

* tmp save group mapping

* add enableControl(not-test)

* verify username exist before create(for LDAP)

* add getAllGroupsDn()

* rename

* adminGroup

* enable TLS Verify

* init for ldap

---------

Co-authored-by: Tao Chen <iamtaochen@outlook.com>
2025-02-02 23:59:52 +08:00
lejianwen
f49457dc5b feat(webclient): Up to 1.3.7 2025-01-21 19:12:28 +08:00
lejianwen
d9e2e247ea feat(api): Add api token expire
Resolves #109
2025-01-21 18:23:28 +08:00
14 changed files with 7770 additions and 7210 deletions

View File

@@ -203,6 +203,7 @@ jobs:
- name: Build package for ${{ matrix.job.platform }} arch
run: |
mv ${{ matrix.job.platform }}/release/apimain debian-build/${{ matrix.job.platform }}/bin/rustdesk-api
mv ${{ matrix.job.platform }}/release/resources/admin resources
chmod -v a+x debian-build/${{ matrix.job.platform }}/bin/*
mkdir -p data
cp -vr debian systemd conf data resources runtime debian-build/${{ matrix.job.platform }}/

View File

@@ -193,6 +193,7 @@ jwt:
| RUSTDESK_API_APP_WEB_CLIENT | 是否启用web-client; 1:启用,0:不启用; 默认启用 | 1 |
| RUSTDESK_API_APP_REGISTER | 是否开启注册; `true`, `false` 默认`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | 是否可见swagger文档;`1`显示,`0`不显示,默认`0`不显示 | `1` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token有效时长 | `3600` |
| -----ADMIN配置----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | 后台标题 | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | 后台欢迎语,可以使用`html` | |

View File

@@ -194,6 +194,7 @@ The prefix for variable names is `RUSTDESK_API`. If environment variables exist,
| RUSTDESK_API_APP_WEB_CLIENT | web client on/off; 1: on, 0 off, default: 1 | 1 |
| RUSTDESK_API_APP_REGISTER | register enable; `true`, `false`; default:`false` | `false` |
| RUSTDESK_API_APP_SHOW_SWAGGER | swagger visible; 1: yes, 0: no; default: 0 | `0` |
| RUSTDESK_API_APP_TOKEN_EXPIRE | token expire duration(second) | `3600` |
| ----- ADMIN Configuration----- | ---------- | ---------- |
| RUSTDESK_API_ADMIN_TITLE | Admin Title | `RustDesk Api Admin` |
| RUSTDESK_API_ADMIN_HELLO | Admin welcome message, you can use `html` | |

View File

@@ -3,6 +3,7 @@ app:
web-client: 1 # 1:启用 0:禁用
register: false #是否开启注册
show-swagger: 0 # 1:启用 0:禁用
token-expire: 360000
admin:
title: "RustDesk Api Admin"
hello-file: "./conf/admin/hello.html" #优先使用file
@@ -57,3 +58,23 @@ oss:
expire-time: 30
max-byte: 10240
ldap:
enable: false
url: "ldap://ldap.example.com:389"
tls: false
tls-verify: false
base-dn: "dc=example,dc=com"
bind-dn: "cn=admin,dc=example,dc=com"
bind-password: "password"
user:
base-dn: "ou=users,dc=example,dc=com"
enable-attr: "" #The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
enable-attr-value: "" # The value of the enable attribute when the user is enabled. If you are using AD, just set random value, it will be ignored.
filter: "(cn=*)"
username: "uid" # The attribute name of the user for usernamem if you are using AD, it should be "sAMAccountName"
email: "mail"
first-name: "givenName"
last-name: "sn"
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.

View File

@@ -17,6 +17,7 @@ type App struct {
WebClient int `mapstructure:"web-client"`
Register bool `mapstructure:"register"`
ShowSwagger int `mapstructure:"show-swagger"`
TokenExpire int `mapstructure:"token-expire"`
}
type Admin struct {
Title string `mapstructure:"title"`
@@ -37,6 +38,7 @@ type Config struct {
Jwt Jwt
Rustdesk Rustdesk
Proxy Proxy
Ldap Ldap
}
// Init 初始化配置

36
config/ldap.go Normal file
View File

@@ -0,0 +1,36 @@
package config
type LdapUser struct {
BaseDn string `mapstructure:"base-dn"` // The base DN of the user for searching
EnableAttr string `mapstructure:"enable-attr"` // The attribute name of the user for enabling, in AD it is "userAccountControl", empty means no enable attribute, all users are enabled
EnableAttrValue string `mapstructure:"enable-attr-value"` // The value of the enable attribute when the user is enabled. If you are using AD, just leave it random str, it will be ignored.
Filter string `mapstructure:"filter"`
Username string `mapstructure:"username"`
Email string `mapstructure:"email"`
FirstName string `mapstructure:"first-name"`
LastName string `mapstructure:"last-name"`
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
}
// type LdapGroup struct {
// BaseDn string `mapstructure:"base-dn"` // The base DN of the group for searching
// Name string `mapstructure:"name"` // The attribute name of the group
// Filter string `mapstructure:"filter"`
// Admin string `mapstructure:"admin"` // Which group is the admin group
// Member string `mapstructure:"member"` // How to get the member of the group: member, uniqueMember, or memberOf (default: member)
// Mode string `mapstructure:"mode"`
// Map map[string]string `mapstructure:"map"` // If mode is "map", map the LDAP group to the internal group
// }
type Ldap struct {
Enable bool `mapstructure:"enable"`
Url string `mapstructure:"url"`
TLS bool `mapstructure:"tls"`
TlsVerify bool `mapstructure:"tls-verify"`
BaseDn string `mapstructure:"base-dn"`
BindDn string `mapstructure:"bind-dn"`
BindPassword string `mapstructure:"bind-password"`
User LdapUser `mapstructure:"user"`
// Group LdapGroup `mapstructure:"group"`
}

13
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/go-playground/validator/v10 v10.11.2
github.com/go-redis/redis/v8 v8.11.4
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.1.2
github.com/google/uuid v1.6.0
github.com/nicksnyder/go-i18n/v2 v2.4.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.8.1
@@ -22,13 +22,14 @@ require (
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.3
golang.org/x/oauth2 v0.23.0
golang.org/x/text v0.18.0
golang.org/x/text v0.21.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.7
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@@ -37,6 +38,8 @@ require (
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
@@ -70,10 +73,10 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect

View File

@@ -32,7 +32,7 @@
<title>RustDesk</title>
<script src="/webclient-config/index.js"></script>
<link rel="manifest" href="manifest.json" />
<script type="module" crossorigin src="js/dist/index.js?v=1bbc8b94"></script>
<script type="module" crossorigin src="js/dist/index.js?v=cabfd933"></script>
<link rel="modulepreload" href="js/dist/vendor.js?v=0b990c6e" />
<style>
html,
@@ -259,7 +259,7 @@
}
scriptLoaded = true;
var scriptTag = document.createElement("script");
scriptTag.src = "main.dart.js?v=f6f842b3";
scriptTag.src = "main.dart.js?v=060a626e";
scriptTag.type = "application/javascript";
document.body.append(scriptTag);
}

View File

@@ -18,7 +18,6 @@ var h = (u, e, i) => (ue(u, e, "read from private field"), i ? i.call(u) : e.get
}
}), je = (u, e, i) => (ue(u, e, "access private method"), i);
const sa = function () {
const e = document.createElement("link").relList;
if (e && e.supports && e.supports("modulepreload")) return;
@@ -7590,7 +7589,7 @@ class N4 {
let i = new Uint8Array(e.data);
this._recvDataCount += i.length;
const o = this._secretKey;
o && (o[2] += 1, i = Mn(i, o[2], o[0]));
o && (o[2] += 1, i = Kn(i, o[2], o[0]));
let a;
i.length == 0 ? a = new Uint8Array : a = this._isRendezvous ? this.parseRendezvous(i) : this.parseMessage(i), this._buf.push(a), this._eventHandlers.message && (this._isProcessing || this.processQueue())
}
@@ -7817,7 +7816,7 @@ const Co = {
RShift: "RShift",
CTRL_ALT_DEL: "CtrlAltDel",
LOCK_SCREEN: "LockScreen"
}, se = "1.3.6", po = "2024-12-22 23:23";
}, se = "1.3.7", po = "2025-01-21 01:12";
class A {
static setItem(e, i) {
@@ -8036,7 +8035,7 @@ async function zo() {
}
function Z(u) {
return Tn(u)
return Ln(u)
}
function c4(u) {
@@ -8044,7 +8043,7 @@ function c4(u) {
}
async function Po() {
return await Jn()
return await $n()
}
function O4() {
@@ -9176,7 +9175,7 @@ async function rn(u) {
function xe(u, e = void 0) {
const i = () => {
try {
Kn(new TextDecoder().decode(u.content)), fe(), e == null || e()
Xn(new TextDecoder().decode(u.content)), fe(), e == null || e()
} catch (o) {
console.error("Failed to copy to clipboard, ", o), document.hasFocus() || (q4 = u)
}
@@ -9240,7 +9239,7 @@ async function Nt(u, e, i = void 0) {
}
function $i() {
In("info", "Clipboard is synchronized", 2e3)
Un("info", "Clipboard is synchronized", 2e3)
}
window.addEventListener("focus", function () {
@@ -9650,7 +9649,7 @@ const hn = async (u, e) => {
o = !1
}
return o && i.push(K4(e, "")), i
}, Ut = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device";
}, Ut = 21116, defaultIdServerPort = 21116, Lt = "rs-ny.rustdesk.com", tt = 100, w4 = "trust-this-device";
class Wt {
constructor() {
@@ -9887,7 +9886,7 @@ class Wt {
(f4 = this._ws) == null || f4.sendMessage({public_key: T});
return
}
const [E, c] = jn(), C = Nn(), D = On(C, l, E), B = K.fromPartial({asymmetric_value: c, symmetric_value: D});
const [E, c] = Wn(), C = Vn(), D = qn(C, l, E), B = K.fromPartial({asymmetric_value: c, symmetric_value: D});
return (x4 = this._ws) == null || x4.sendMessage({public_key: B}), (Ie = this._ws) == null || Ie.setSecretKey(C), console.log("secured"), !0
}
@@ -9905,7 +9904,7 @@ class Wt {
Re(o.colors, !1, a => {
a && (o.colors = a, m("cursor_data", o))
})
} else if (e != null && e.cursor_id) m("cursor_id", {id: e == null ? void 0 : e.cursor_id}); else if (e != null && e.cursor_position) m("cursor_position", e == null ? void 0 : e.cursor_position); else if (e != null && e.misc) this.handleMisc(e == null ? void 0 : e.misc); else if (e != null && e.audio_frame) Vn(e == null ? void 0 : e.audio_frame.data); else if (e != null && e.message_box) this.handleMsgBox(e == null ? void 0 : e.message_box); else if (e != null && e.peer_info) this.handleSyncPeerInfo(e.peer_info); else if (e.file_response) await this.handleFileResponse(e.file_response); else if (e.file_action) {
} else if (e != null && e.cursor_id) m("cursor_id", {id: e == null ? void 0 : e.cursor_id}); else if (e != null && e.cursor_position) m("cursor_position", e == null ? void 0 : e.cursor_position); else if (e != null && e.misc) this.handleMisc(e == null ? void 0 : e.misc); else if (e != null && e.audio_frame) Zn(e == null ? void 0 : e.audio_frame.data); else if (e != null && e.message_box) this.handleMsgBox(e == null ? void 0 : e.message_box); else if (e != null && e.peer_info) this.handleSyncPeerInfo(e.peer_info); else if (e.file_response) await this.handleFileResponse(e.file_response); else if (e.file_action) {
const o = e.file_action;
await this.handleFileAction(o)
}
@@ -10324,7 +10323,7 @@ class Wt {
}
handleMisc(e) {
if (e.audio_format) Wn(e.audio_format.channels, e.audio_format.sample_rate); else if (e.chat_message) m("chat_client_mode", {text: e.chat_message.text}); else if (e.permission_info) {
if (e.audio_format) Gn(e.audio_format.channels, e.audio_format.sample_rate); else if (e.chat_message) m("chat_client_mode", {text: e.chat_message.text}); else if (e.permission_info) {
const i = e.permission_info;
console.info("Change permission " + i.permission + " -> " + i.enabled);
let o;
@@ -10494,7 +10493,7 @@ class Wt {
inputKey(e, i, o, a, t, s, l) {
var c;
const E = So(e, Rn());
const E = So(e, Mn());
!E || (a && (e == "VK_MENU" || e == "RAlt") && (a = !1), t && (e == "VK_CONTROL" || e == "RControl") && (t = !1), s && (e == "VK_SHIFT" || e == "RShift") && (s = !1), l && (e == "Meta" || e == "RWin") && (l = !1), E.down = i, E.press = o, E.modifiers = this.getMod(a, t, s, l), (c = this._ws) == null || c.sendMessage({key_event: E}))
}
@@ -11088,7 +11087,7 @@ function R4(u = !1) {
return I4(e || Lt, u)
}
function getrUriFromRs(uri, isRelay = false, roffset = 0) {
function getUriFromRs(uri, isRelay = false, roffset = 0) {
const p = isHttps() ? "wss://" : "ws://"
const [domain, uriport] = uri.split(":")
if (isHttps() && (!uriport)) {
@@ -11097,7 +11096,7 @@ function getrUriFromRs(uri, isRelay = false, roffset = 0) {
if (uriport) {
const port = parseInt(uriport);
uri = domain + ":" + (port + (isRelay ? roffset || 3 : 2))
} else uri += ":" + (Ut + (isRelay ? 3 : 2));
} else uri += ":" + (defaultIdServerPort + (isRelay ? 3 : 2));
return p + uri
}
@@ -11106,7 +11105,7 @@ function isHttps() {
}
function I4(u, e = !1, i = 0) {
return getrUriFromRs(u, e, i)
return getUriFromRs(u, e, i)
}
function wn() {
@@ -11117,7 +11116,6 @@ function Sn(u) {
return u.indexOf(":") > 0 ? u.split(":")[0] : u
}
const at = (u, e, i) => e && u.type == "SharedAb" ? Z(Zu([u.value, i.salt])) === Z(e) : !1,
ot = (u, e) => e && u.type == "PersonalAb" ? Z(u.value) === Z(e) : !1;
@@ -11244,11 +11242,63 @@ async function Pn(u) {
}
const Rn = "rustdesk-client";
function In() {
if (typeof navigator != "undefined") {
const u = navigator.platform.toLowerCase();
return u.includes("win") ? "windows" : u.includes("mac") ? "macos" : u.includes("linux") ? "linux" : u
}
return "unknown"
}
function Tn() {
const u = In();
return u === "windows" ? navigator.userAgent.includes("Win64") ? "x86_64" : "x86" : u === "macos" ? navigator.userAgent.includes("Intel") ? "x86_64" : "arm64" : navigator.userAgent.includes("x64") ? "x86_64" : "x86"
}
function jn() {
const u = navigator.userAgent;
let e = "", i = "";
if (u.includes("Windows")) {
e = "windows";
const o = u.match(/Windows NT (\d+\.\d+)/);
o && (i = o[1])
} else if (u.includes("Mac OS X")) {
e = "macos";
const o = u.match(/Mac OS X (\d+[._]\d+[._]\d+)/);
o && (i = o[1].replace(/_/g, "."))
} else if (u.includes("Linux")) {
e = "linux";
const o = u.match(/Linux\s*([\d.]+)?/);
o && o[1] && (i = o[1])
} else e = "unknown", i = "";
return e += "-" + navigator.userAgent, {os: e, os_version: i}
}
async function Nn(u) {
const e = "https://api.rustdesk.com/version/latest", {os: i, os_version: o} = jn(), a = Tn();
return [{os: i, os_version: o, arch: a, device_id: [], typ: u}, e]
}
async function On() {
try {
const [u, e] = await Nn(Rn);
return await (await fetch(e, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(u)
})).json()
} catch {
return null
}
}
window.curConn = void 0;
window.isMobile = () => /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
const ye = zt(), Yu = ye === Y4, H4 = ye === Be, Qu = ye === me;
function Rn() {
function Mn() {
return !isMobile()
}
@@ -11267,7 +11317,7 @@ function Vt(u, e, i, o) {
}
}
function In(u, e, i) {
function Un(u, e, i) {
onGlobalEvent(JSON.stringify({name: "toast", type: u, text: e, dur_msec: i}))
}
@@ -11340,20 +11390,20 @@ function Zt(u) {
return q.from_base64(u, q.base64_variants.ORIGINAL)
}
function Tn(u) {
function Ln(u) {
return q.to_base64(u, q.base64_variants.ORIGINAL)
}
function jn() {
function Wn() {
const u = q.crypto_box_keypair(), e = u.privateKey, i = u.publicKey;
return [e, i]
}
function Nn() {
function Vn() {
return q.crypto_secretbox_keygen()
}
function On(u, e, i) {
function qn(u, e, i) {
const o = Uint8Array.from(Array(24).fill(0));
return q.crypto_box_easy(u, o, e, i)
}
@@ -11370,7 +11420,7 @@ function st(u, e, i) {
return q.crypto_secretbox_easy(u, $4(e), i)
}
function Mn(u, e, i) {
function Kn(u, e, i) {
return q.crypto_secretbox_open_easy(u, $4(e), i)
}
@@ -11428,7 +11478,7 @@ window.setByName = (u, e) => {
e = JSON.parse(e), Ho(curConn, e.usb_hid, e.down == "true", e.lock_modes);
break;
case"send_mouse":
Un(e);
Hn(e);
break;
case"send_2fa":
curConn == null || curConn.send2fa(e);
@@ -11455,7 +11505,7 @@ window.setByName = (u, e) => {
e = JSON.parse(e), curConn.setFlutterUiOption(e.name, e.value);
break;
case"option:user:default":
Gn(e);
ur(e);
break;
case"option:session":
e = JSON.parse(e), curConn.setOption(e.name, e.value);
@@ -11473,12 +11523,12 @@ window.setByName = (u, e) => {
curConn.inputOsPassword(e);
break;
case"session_add_sync":
return Xn(e);
return tr(e);
case"session_start":
Yn();
ar();
break;
case"session_close":
$n();
or();
break;
case"elevate_direct":
curConn.elevateDirect();
@@ -11500,13 +11550,13 @@ window.setByName = (u, e) => {
curConn.changePreferCodec(e);
break;
case"cursor":
Hn(e);
Yn(e);
break;
case"enter_or_leave":
curConn == null || curConn.enterOrLeave(e);
break;
case"fullscreen":
e == "Y" ? er() : ir();
e == "Y" ? rr() : sr();
break;
case"send_note":
const i = Yt("conn");
@@ -11549,7 +11599,7 @@ window.setByName = (u, e) => {
curConn == null || curConn.sendChat(e);
break;
case"load_ab":
or();
dr();
break;
case"save_ab":
_o(e);
@@ -11558,7 +11608,7 @@ window.setByName = (u, e) => {
vo();
break;
case"load_group":
nr();
cr();
break;
case"save_group":
ko(e);
@@ -11575,7 +11625,7 @@ window.setByName = (u, e) => {
}
};
function Un(u) {
function Hn(u) {
if (!curConn) return;
let e = 0;
switch (u = JSON.parse(u), u.type) {
@@ -11618,11 +11668,11 @@ function Un(u) {
}
window.getByName = (u, e) => {
let i = Ln(u, e);
let i = Jn(u, e);
return typeof i == "string" || i instanceof String ? i : i == null || i == null ? "" : JSON.stringify(i)
};
function Ln(u, e) {
function Jn(u, e) {
var o, a, t, s;
switch (u) {
case"remember":
@@ -11669,10 +11719,10 @@ function Ln(u, e) {
case"version":
return se;
case"load_recent_peers":
Zn();
er();
break;
case"load_fav_peers":
Qn();
ir();
break;
case"fav":
return (a = A.getItem("fav")) != null ? a : "[]";
@@ -11726,7 +11776,7 @@ function Ln(u, e) {
case"peer_has_password":
return ((t = (Cu()[e] || {}).password) != null ? t : "") !== "";
case"fullscreen":
return tr() ? "Y" : "N";
return lr() ? "Y" : "N";
case"platform":
return curConn.getPlatform();
case"enable_trusted_devices":
@@ -11737,11 +11787,11 @@ function Ln(u, e) {
let ze = new Worker("./libopus.js?v=02816afa"), Qt;
function Wn(u, e) {
Qt = qn(u, e), ze.postMessage({channels: u, sampleRate: e})
function Gn(u, e) {
Qt = Qn(u, e), ze.postMessage({channels: u, sampleRate: e})
}
function Vn(u) {
function Zn(u) {
ze.postMessage(u, [u.buffer])
}
@@ -11749,7 +11799,7 @@ window.init = async () => {
try {
ze.onmessage = u => {
Qt.feed(u.data)
}, await Jt(), await zo(), await Pa(), await N.init(), console.log("init done"), onInitFinished()
}, await Jt(), await zo(), await Pa(), await N.init(), console.log("init done"), onInitFinished(), await On()
} catch (u) {
console.error("Failed to init: " + u.message), onInitFinished()
}
@@ -11758,11 +11808,11 @@ window.onunload = () => {
console.log("window close"), Ia()
};
function qn(u, e) {
function Qn(u, e) {
return new ra({channels: u, sampleRate: e, flushingTime: 2e3})
}
function Kn(u) {
function Xn(u) {
if (window.clipboardData && window.clipboardData.setData) return window.clipboardData.setData("Text", u);
if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
var e = document.createElement("textarea");
@@ -11791,7 +11841,7 @@ function Q(u) {
}
}
function Hn(u) {
function Yn(u) {
let e = "auto";
if (u != "auto") try {
const t = JSON.parse(u);
@@ -11807,13 +11857,13 @@ function Hn(u) {
}
}
async function Jn() {
async function $n() {
await T4.ready;
const u = T4.crypto_sign_keypair();
return {publicKey: u.publicKey, privateKey: u.privateKey}
}
function Gn(u) {
function ur(u) {
try {
const e = JSON.parse(u), i = JSON.parse(A.getItem("user-default-options")) || {};
i[e.name] = e.value, A.setItem("user-default-options", JSON.stringify(i))
@@ -11855,22 +11905,23 @@ function Pe() {
return u.sort().reverse().map(e => e[2])
}
function Zn() {
function er() {
const u = Pe();
u && be("load_recent_peers", {peers: JSON.stringify(u)})
}
function Qn() {
function ir() {
var u;
try {
const e = (u = A.getItem("fav")) != null ? u : "[]", i = JSON.parse(e), o = Pe().filter(a => i.includes(a.id));
const e = (u = A.getItem("fav")) != null ? u : "[]", i = JSON.parse(e),
o = Pe().filter(a => i.includes(a.id));
o && be("load_fav_peers", {peers: JSON.stringify(o)})
} catch (e) {
console.error("Failed to load fav peers: " + e.message)
}
}
function Xn(u) {
function tr(u) {
var e;
try {
const i = JSON.parse(u), o = i.id;
@@ -11884,7 +11935,7 @@ function Xn(u) {
}
}
function Yn(u) {
function ar(u) {
try {
if (!e0()) return;
Kt()
@@ -11893,11 +11944,11 @@ function Yn(u) {
}
}
function $n(u) {
function or(u) {
Se()
}
function ur(u, e) {
function nr(u, e) {
function i(o) {
return /^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$/.test(o)
}
@@ -11925,7 +11976,7 @@ function Xt() {
if (u) return u;
const e = A.getItem("custom-rendezvous-server");
if (e) {
let i = ur(e, -2);
let i = nr(e, -2);
return i == e ? `http://${i}:${Ut - 2}` : `http://${i}`
}
return "https://admin.rustdesk.com"
@@ -11985,28 +12036,28 @@ async function ea(u, e) {
})
}
function er() {
function rr() {
const u = document.documentElement;
u.requestFullscreen ? u.requestFullscreen() : u.mozRequestFullScreen ? u.mozRequestFullScreen() : u.webkitRequestFullscreen ? u.webkitRequestFullscreen() : u.msRequestFullscreen && u.msRequestFullscreen()
}
function ir() {
function sr() {
document.exitFullscreen ? document.exitFullscreen() : document.mozCancelFullScreen ? document.mozCancelFullScreen() : document.webkitExitFullscreen ? document.webkitExitFullscreen() : document.msExitFullscreen && document.msExitFullscreen()
}
function tr() {
function lr() {
return document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement
}
var lt = !1;
function ar() {
function Er() {
lt || (console.log("listen fullscreen"), lt = !0, document.addEventListener("fullscreenchange", () => onFullscreenChanged(!!document.fullscreenElement)), document.addEventListener("mozfullscreenchange", () => onFullscreenChanged(!!document.mozFullScreen)), document.addEventListener("webkitfullscreenchange", () => onFullscreenChanged(!!document.webkitFullscreenElement)), document.addEventListener("msfullscreenchange", () => onFullscreenChanged(!!document.msFullscreenElement)))
}
ar();
Er();
async function or() {
async function dr() {
try {
let u = await xt();
onLoadAbFinished(JSON.stringify(u))
@@ -12015,7 +12066,7 @@ async function or() {
}
}
async function nr() {
async function cr() {
try {
let u = await go();
onLoadGroupFinished(JSON.stringify(u))
@@ -12077,4 +12128,4 @@ if (Et) {
const i = document.querySelector("input#password").value;
i && (document.querySelector("div#password").style.display = "none", e0().login(i))
}
}
}

View File

@@ -4685,7 +4685,7 @@ Kui soovid juurdep\xE4\xE4su seadmele avalikus serveris, sisesta "<id>@public",
}, sl: {
Status: "Stanje",
"Your Desktop": "Va\u0161e namizje",
desk_tip: "Do va\u0161ega namizja lahko dostopate s spodnjim IDjem in geslom",
desk_tip: "S spodnjim IDjem in geslom omogo\u010Dite oddaljeni nadzor va\u0161ega ra\u010Dunalnika",
Password: "Geslo",
Ready: "Pripravljen",
Established: "Povezava vzpostavljena",
@@ -4872,7 +4872,7 @@ Kui soovid juurdep\xE4\xE4su seadmele avalikus serveris, sisesta "<id>@public",
"Logging in...": "Prijavljanje...",
"Enable RDP session sharing": "Omogo\u010Di deljenje RDP seje",
"Auto Login": "Samodejna prijava",
"Enable direct IP access": "Omogo\u010Di neposredni dostop preko IP",
"Enable direct IP access": "Omogo\u010Di neposredni dostop preko IP naslova",
Rename: "Preimenuj",
Space: "Prazno",
"Create desktop shortcut": "Ustvari bli\u017Enjico na namizju",
@@ -5046,7 +5046,7 @@ Kui soovid juurdep\xE4\xE4su seadmele avalikus serveris, sisesta "<id>@public",
Recording: "Snemanje",
Directory: "Imenik",
"Automatically record incoming sessions": "Samodejno snemaj vhodne seje",
"Automatically record outgoing sessions": "",
"Automatically record outgoing sessions": "Samodejno snemaj odhodne seje",
Change: "Spremeni",
"Start session recording": "Za\u010Dni snemanje seje",
"Stop session recording": "Ustavi snemanje seje",
@@ -5094,8 +5094,8 @@ Kui soovid juurdep\xE4\xE4su seadmele avalikus serveris, sisesta "<id>@public",
"Select local keyboard type": "Izberite lokalno vrsto tipkovnice",
software_render_tip: "\u010Ce na Linuxu uporabljate Nvidino grafi\u010Dno kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa.",
"Always use software rendering": "Vedno uporabi programsko upodabljanje",
config_input: "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico \xBBNadzor vnosa\xAB.",
config_microphone: "Za zajem zvoka, rabi RustDesk pravico \xBBSnemanje zvoka\xAB.",
config_input: "RustDesk potrebuje pravico \xBBNadzor vnosa\xAB za nadzor oddaljenega namizja s tipkovnico.",
config_microphone: "RustDesk potrebuje pravico \xBBSnemanje zvoka\xAB za zajemanje zvoka.",
request_elevation_tip: "Lahko tudi zaprosite za dvig pravic, \u010De je kdo na oddaljeni strani.",
Wait: "\u010Cakaj",
"Elevation Error": "Napaka pri povzdigovanju",
@@ -5128,7 +5128,7 @@ Kui soovid juurdep\xE4\xE4su seadmele avalikus serveris, sisesta "<id>@public",
"Voice call": "Glasovni klic",
"Text chat": "Besedilni klepet",
"Stop voice call": "Prekini glasovni klic",
relay_hint_tip: "Morda neposredna povezava ni mo\u017Ena; lahko se poikusite povezati preko posrednika. \u010Ce \u017Eelite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate \xBB/r\xAB, ali pa izberete mo\u017Enost \xBBVedno pove\u017Ei preko posrednika\xAB v kartici nedavnih sej, \u010De le-ta obstja.",
relay_hint_tip: "Morda neposredna povezava ni mo\u017Ena; lahko se poizkusite povezati preko posrednika. \u010Ce \u017Eelite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate \xBB/r\xAB, ali pa izberete mo\u017Enost \xBBVedno pove\u017Ei preko posrednika\xAB v kartici nedavnih sej, \u010De le-ta obstja.",
Reconnect: "Ponovna povezava",
Codec: "Kodek",
Resolution: "Lo\u010Dljivost",
@@ -5343,13 +5343,13 @@ Lahko se pove\u017Eete na druge naprave, druge naprave pa se k vam ne morejo pov
web_id_input_tip: `Vnesete lahko ID iz istega stre\u017Enika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.
\u010Ce \u017Eelite dostopati do naprave na drugem stre\u017Eniku, pripnite naslov stre\u017Enika (<id>@<naslov_stre\u017Enika>?key=<klju\u010D>), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.
\u010Ce \u017Eelite dostopati do naprave na javnem stre\u017Eniku, vnesite \xBB<id>@public\xAB; klju\u010D za javni stre\u017Enik ni potreben.`,
Download: "",
"Upload folder": "",
"Upload files": "",
"Clipboard is synchronized": "",
"Update client clipboard": "",
Untagged: "",
"new-version-of-{}-tip": ""
Download: "Prenos",
"Upload folder": "Nalo\u017Ei mapo",
"Upload files": "Nalo\u017Ei datoteke",
"Clipboard is synchronized": "Odlo\u017Ei\u0161\u010De je usklajeno",
"Update client clipboard": "Osve\u017Ei odjemal\u010Devo odlo\u017Ei\u0161\u010De",
Untagged: "Neozna\u010Deno",
"new-version-of-{}-tip": "Na voljo je nova razli\u010Dica {}"
}, ko: {
Status: "\uC0C1\uD0DC",
"Your Desktop": "\uB0B4 \uB370\uC2A4\uD06C\uD0D1",
@@ -6683,7 +6683,7 @@ Ja v\u0113laties piek\u013C\u016Bt ier\u012Bcei publiskaj\u0101 server\u012B, l\
"Clipboard is synchronized": "Starpliktuve ir sinhroniz\u0113ta",
"Update client clipboard": "Atjaunin\u0101t klienta starpliktuvi",
Untagged: "Neatz\u012Bm\u0113ts",
"new-version-of-{}-tip": ""
"new-version-of-{}-tip": "Ir pieejama jauna {} versija"
}, pl: {
Status: "Status",
"Your Desktop": "Tw\xF3j pulpit",
@@ -18966,7 +18966,7 @@ H\xE3y t\xECm ai \u0111\xF3 \u0111\u1EC3 k\u1EBFt n\u1ED1i c\xF9ng v\xE0 th\xEAm
Dark: "\u9ED1\u6697",
Light: "\u660E\u4EAE",
"Follow System": "\u8DDF\u968F\u7CFB\u7EDF",
"Enable hardware codec": "\u4F7F\u80FD\u786C\u4EF6\u7F16\u89E3\u7801",
"Enable hardware codec": "\u542F\u7528\u786C\u4EF6\u7F16\u89E3\u7801",
"Unlock Security Settings": "\u89E3\u9501\u5B89\u5168\u8BBE\u7F6E",
"Enable audio": "\u5141\u8BB8\u4F20\u8F93\u97F3\u9891",
"Unlock Network Settings": "\u89E3\u9501\u7F51\u7EDC\u8BBE\u7F6E",
@@ -21967,8 +21967,8 @@ Si quieres accedder a un dispositivo en un servidor p\xFAblico, por favor, intro
"Upload files": "Subir archivos",
"Clipboard is synchronized": "Portapapeles sincronizado",
"Update client clipboard": "Actualizar portapapeles del cliente",
Untagged: "",
"new-version-of-{}-tip": ""
Untagged: "Sin itiquetar",
"new-version-of-{}-tip": "Hay una nueva versi\xF3n de {} disponible"
}, sr: {
Status: "Status",
"Your Desktop": "Va\u0161a radna povr\u0161ina",
@@ -23662,7 +23662,7 @@ Ha egy nyilv\xE1nos kiszolg\xE1l\xF3n l\xE9v\u0151 eszk\xF6zh\xF6z szeretne hozz
Recording: "\u9304\u88FD",
Directory: "\u8DEF\u5F91",
"Automatically record incoming sessions": "\u81EA\u52D5\u9304\u88FD\u9023\u5165\u7684\u5DE5\u4F5C\u968E\u6BB5",
"Automatically record outgoing sessions": "",
"Automatically record outgoing sessions": "\u81EA\u52D5\u9304\u88FD\u9023\u51FA\u7684\u5DE5\u4F5C\u968E\u6BB5",
Change: "\u8B8A\u66F4",
"Start session recording": "\u958B\u59CB\u9304\u5F71",
"Stop session recording": "\u505C\u6B62\u9304\u5F71",

File diff suppressed because one or more lines are too long

425
service/ldap.go Normal file
View File

@@ -0,0 +1,425 @@
package service
import (
"crypto/tls"
"errors"
"fmt"
"github.com/go-ldap/ldap/v3"
"strconv"
"strings"
"Gwen/config"
"Gwen/global"
"Gwen/model"
)
// LdapService is responsible for LDAP authentication and user synchronization.
type LdapService struct {
}
// LdapUser represents the user attributes retrieved from LDAP.
type LdapUser struct {
Dn string
Username string
Email string
FirstName string
LastName string
MemberOf []string
EnableAttrValue string
Enabled bool
}
// Name returns the full name of an LDAP user.
func (lu *LdapUser) Name() string {
return fmt.Sprintf("%s %s", lu.FirstName, lu.LastName)
}
// ToUser merges the LdapUser data into a provided *model.User.
// If 'u' is nil, it creates and returns a new *model.User.
func (lu *LdapUser) ToUser(u *model.User) *model.User {
if u == nil {
u = &model.User{}
}
u.Username = lu.Username
u.Email = lu.Email
u.Nickname = lu.Name()
return u
}
// connectAndBind creates an LDAP connection, optionally starts TLS, and then binds using the provided credentials.
func (ls *LdapService) connectAndBind(cfg *config.Ldap, username, password string) (*ldap.Conn, error) {
conn, err := ldap.DialURL(cfg.Url)
if err != nil {
return nil, fmt.Errorf("failed to dial LDAP: %w", err)
}
if cfg.TLS {
// WARNING: InsecureSkipVerify: true is not recommended for production
if err = conn.StartTLS(&tls.Config{InsecureSkipVerify: !cfg.TlsVerify}); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to start TLS: %w", err)
}
}
// Bind as the "service" user
if err = conn.Bind(username, password); err != nil {
conn.Close()
return nil, fmt.Errorf("failed to bind with service account: %w", err)
}
return conn, nil
}
// connectAndBindAdmin creates an LDAP connection, optionally starts TLS, and then binds using the admin credentials.
func (ls *LdapService) connectAndBindAdmin(cfg *config.Ldap) (*ldap.Conn, error) {
return ls.connectAndBind(cfg, cfg.BindDn, cfg.BindPassword)
}
// verifyCredentials checks the provided username and password against LDAP.
func (ls *LdapService) verifyCredentials(cfg *config.Ldap, username, password string) error {
ldapConn, err := ls.connectAndBind(cfg, username, password)
if err != nil {
return err
}
defer ldapConn.Close()
return nil
}
// Authenticate checks the provided username and password against LDAP.
// Returns the corresponding *model.User if successful, or an error if not.
func (ls *LdapService) Authenticate(username, password string) (*model.User, error) {
cfg := &global.Config.Ldap
// 1. Use a service bind to search for the user DN
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return nil, fmt.Errorf("LDAP search request failed: %w", err)
}
if len(sr.Entries) != 1 {
return nil, errors.New("user does not exist or too many entries returned")
}
entry := sr.Entries[0]
userDN := entry.DN
err = ls.verifyCredentials(cfg, userDN, password)
if err != nil {
return nil, fmt.Errorf("LDAP authentication failed: %w", err)
}
ldapUser := ls.userResultToLdapUser(cfg, entry)
if !ldapUser.Enabled {
return nil, errors.New("UserDisabledAtLdap")
}
user, err := ls.mapToLocalUser(cfg, ldapUser)
if err != nil {
return nil, fmt.Errorf("failed to map LDAP user to local user: %w", err)
}
return user, nil
}
// mapToLocalUser checks whether the user exists locally; if not, creates one.
// If the user exists and Ldap.Sync is enabled, it updates local info.
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
userService := &UserService{}
localUser := userService.InfoByUsername(lu.Username)
isAdmin := ls.isUserAdmin(cfg, lu)
// If the user doesn't exist in local DB, create a new one
if localUser.Id == 0 {
newUser := lu.ToUser(nil)
// Typically, you dont store LDAP user passwords locally.
// If needed, you can set a random password here.
newUser.IsAdmin = &isAdmin
if err := global.DB.Create(newUser).Error; err != nil {
return nil, fmt.Errorf("failed to create new user: %w", err)
}
return userService.InfoByUsername(lu.Username), nil
}
// If the user already exists and sync is enabled, update local info
if cfg.User.Sync {
originalEmail := localUser.Email
originalNickname := localUser.Nickname
originalIsAdmin := localUser.IsAdmin
lu.ToUser(localUser) // merges LDAP data into the existing user
localUser.IsAdmin = &isAdmin
if err := userService.Update(localUser); err != nil {
// If the update fails, revert to original data
localUser.Email = originalEmail
localUser.Nickname = originalNickname
localUser.IsAdmin = originalIsAdmin
}
}
return localUser, nil
}
// IsUsernameExists checks if a username exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsUsernameExists(username string) bool {
cfg := &global.Config.Ldap
if !cfg.Enable {
return false
}
sr, err := ls.usernameSearchResult(cfg, username)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// IsEmailExists checks if an email exists in LDAP (can be useful for local registration checks).
func (ls *LdapService) IsEmailExists(email string) bool {
cfg := &global.Config.Ldap
if !cfg.Enable {
return false
}
sr, err := ls.emailSearchResult(cfg, email)
if err != nil {
return false
}
return len(sr.Entries) > 0
}
// usernameSearchResult returns the search result for the given username.
func (ls *LdapService) usernameSearchResult(cfg *config.Ldap, username string) (*ldap.SearchResult, error) {
// Build the combined filter for the username
filter := ls.filterField(ls.fieldUsername(cfg), username)
// Create the *ldap.SearchRequest
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
// emailSearchResult returns the search result for the given email.
func (ls *LdapService) emailSearchResult(cfg *config.Ldap, email string) (*ldap.SearchResult, error) {
filter := ls.filterField(ls.fieldEmail(cfg), email)
searchRequest := ls.buildUserSearchRequest(cfg, filter)
return ls.searchResult(cfg, searchRequest)
}
func (ls *LdapService) searchResult(cfg *config.Ldap, searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
ldapConn, err := ls.connectAndBindAdmin(cfg)
if err != nil {
return nil, err
}
defer ldapConn.Close()
return ldapConn.Search(searchRequest)
}
// buildUserSearchRequest constructs an LDAP SearchRequest for users given a filter.
func (ls *LdapService) buildUserSearchRequest(cfg *config.Ldap, filter string) *ldap.SearchRequest {
baseDn := ls.baseDnUser(cfg) // user-specific base DN, or fallback
filterConfig := cfg.User.Filter
if filterConfig == "" {
filterConfig = "(cn=*)"
}
// Combine the default filter with our field filter, e.g. (&(cn=*)(uid=jdoe))
combinedFilter := fmt.Sprintf("(&%s%s)", filterConfig, filter)
attributes := ls.buildUserAttributes(cfg)
return ldap.NewSearchRequest(
baseDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
combinedFilter,
attributes,
nil,
)
}
// buildUserAttributes returns the list of attributes we want from LDAP user searches.
func (ls *LdapService) buildUserAttributes(cfg *config.Ldap) []string {
return []string{
"dn",
ls.fieldUsername(cfg),
ls.fieldEmail(cfg),
ls.fieldFirstName(cfg),
ls.fieldLastName(cfg),
ls.fieldMemberOf(),
ls.fieldUserEnableAttr(cfg),
}
}
// userResultToLdapUser maps an *ldap.Entry to our LdapUser struct.
func (ls *LdapService) userResultToLdapUser(cfg *config.Ldap, entry *ldap.Entry) *LdapUser {
lu := &LdapUser{
Dn: entry.DN,
Username: entry.GetAttributeValue(ls.fieldUsername(cfg)),
Email: entry.GetAttributeValue(ls.fieldEmail(cfg)),
FirstName: entry.GetAttributeValue(ls.fieldFirstName(cfg)),
LastName: entry.GetAttributeValue(ls.fieldLastName(cfg)),
MemberOf: entry.GetAttributeValues(ls.fieldMemberOf()),
EnableAttrValue: entry.GetAttributeValue(ls.fieldUserEnableAttr(cfg)),
}
// Check if the user is enabled based on the LDAP configuration
ls.isUserEnabled(cfg, lu)
return lu
}
// filterField helps build simple attribute filters, e.g. (uid=username).
func (ls *LdapService) filterField(field, value string) string {
return fmt.Sprintf("(%s=%s)", field, value)
}
// fieldUsername returns the configured username attribute or "uid" if not set.
func (ls *LdapService) fieldUsername(cfg *config.Ldap) string {
if cfg.User.Username == "" {
return "uid"
}
return cfg.User.Username
}
// fieldEmail returns the configured email attribute or "mail" if not set.
func (ls *LdapService) fieldEmail(cfg *config.Ldap) string {
if cfg.User.Email == "" {
return "mail"
}
return cfg.User.Email
}
// fieldFirstName returns the configured first name attribute or "givenName" if not set.
func (ls *LdapService) fieldFirstName(cfg *config.Ldap) string {
if cfg.User.FirstName == "" {
return "givenName"
}
return cfg.User.FirstName
}
// fieldLastName returns the configured last name attribute or "sn" if not set.
func (ls *LdapService) fieldLastName(cfg *config.Ldap) string {
if cfg.User.LastName == "" {
return "sn"
}
return cfg.User.LastName
}
func (ls *LdapService) fieldMemberOf() string {
return "memberOf"
}
func (ls *LdapService) fieldUserEnableAttr(cfg *config.Ldap) string {
if cfg.User.EnableAttr == "" {
return "userAccountControl"
}
return cfg.User.EnableAttr
}
// baseDnUser returns the user-specific base DN or the global base DN if none is set.
func (ls *LdapService) baseDnUser(cfg *config.Ldap) string {
if cfg.User.BaseDn == "" {
return cfg.BaseDn
}
return cfg.User.BaseDn
}
// isUserAdmin checks if the user is a member of the admin group.
func (ls *LdapService) isUserAdmin(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Check if the admin group is configured
adminGroup := cfg.User.AdminGroup
if adminGroup == "" {
return false
}
// Check "memberOf" directly
if len(ldapUser.MemberOf) > 0 {
for _, group := range ldapUser.MemberOf {
if group == adminGroup {
return true
}
}
return false
}
// For "member" attribute, perform a reverse search on the group
member := "member"
userDN := ldap.EscapeFilter(ldapUser.Dn)
adminGroupDn := ldap.EscapeFilter(adminGroup)
groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
// Create the LDAP search request
groupSearchRequest := ldap.NewSearchRequest(
adminGroupDn,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0, // Unlimited search results
0, // No time limit
false, // Return both attributes and DN
groupFilter,
[]string{"dn"},
nil,
)
// Perform the group search
groupResult, err := ls.searchResult(cfg, groupSearchRequest)
if err != nil {
return false
}
// If any results are returned, the user is part of the admin group
if len(groupResult.Entries) > 0 {
return true
}
return false
}
// isUserEnabled checks if the user is enabled based on the LDAP configuration.
// If no enable attribute or value is set, all users are considered enabled by default.
func (ls *LdapService) isUserEnabled(cfg *config.Ldap, ldapUser *LdapUser) bool {
// Retrieve the enable attribute and expected value from the configuration
enableAttr := cfg.User.EnableAttr
enableAttrValue := cfg.User.EnableAttrValue
// If no enable attribute or value is configured, consider all users as enabled
if enableAttr == "" || enableAttrValue == "" {
ldapUser.Enabled = true
return true
}
// Normalize the enable attribute for comparison
enableAttr = strings.ToLower(enableAttr)
// Handle Active Directory's userAccountControl attribute
if enableAttr == "useraccountcontrol" {
// Parse the userAccountControl value
userAccountControl, err := strconv.Atoi(ldapUser.EnableAttrValue)
if err != nil {
fmt.Printf("[ERROR] Invalid userAccountControl value: %v\n", err)
ldapUser.Enabled = false
return false
}
// Account is disabled if the ACCOUNTDISABLE flag (0x2) is set
const ACCOUNTDISABLE = 0x2
ldapUser.Enabled = (userAccountControl&ACCOUNTDISABLE == 0)
return ldapUser.Enabled
}
// For other attributes, perform a direct comparison with the expected value
ldapUser.Enabled = (ldapUser.EnableAttrValue == enableAttrValue)
return ldapUser.Enabled
}
// getAttrOfDn retrieves the value of an attribute for a given DN.
func (ls *LdapService) getAttrOfDn(cfg *config.Ldap, dn, attr string) string {
searchRequest := ldap.NewSearchRequest(
ldap.EscapeFilter(dn),
ldap.ScopeBaseObject,
ldap.NeverDerefAliases,
0, // unlimited search results
0, // no server-side time limit
false, // typesOnly
"(objectClass=*)",
[]string{attr},
nil,
)
sr, err := ls.searchResult(cfg, searchRequest)
if err != nil {
return ""
}
if len(sr.Entries) == 0 {
return ""
}
return sr.Entries[0].GetAttributeValue(attr)
}

View File

@@ -18,6 +18,7 @@ type Service struct {
*AuditService
*ShareRecordService
*ServerCmdService
*LdapService
}
func New() *Service {

View File

@@ -46,6 +46,14 @@ func (us *UserService) InfoByOpenid(openid string) *model.User {
// InfoByUsernamePassword 根据用户名密码取用户信息
func (us *UserService) InfoByUsernamePassword(username, password string) *model.User {
if global.Config.Ldap.Enable {
u, err := AllService.LdapService.Authenticate(username, password)
if err == nil {
return u
}
global.Logger.Error("LDAP authentication failed, %v", err)
global.Logger.Warn("Fallback to local database")
}
u := &model.User{}
global.DB.Where("username = ? and password = ?", username, us.EncryptPassword(password)).First(u)
return u
@@ -82,7 +90,7 @@ func (us *UserService) Login(u *model.User, llog *model.LoginLog) *model.UserTok
Token: token,
DeviceUuid: llog.Uuid,
DeviceId: llog.DeviceId,
ExpiredAt: time.Now().Add(time.Hour * 24 * 7).Unix(),
ExpiredAt: us.UserTokenExpireTimestamp(),
}
global.DB.Create(ut)
llog.UserTokenId = ut.UserId
@@ -156,6 +164,9 @@ func (us *UserService) CheckUserEnable(u *model.User) bool {
// Create 创建
func (us *UserService) Create(u *model.User) error {
// The initial username should be formatted, and the username should be unique
if us.IsUsernameExists(u.Username) {
return errors.New("UsernameExists")
}
u.Username = us.formatUsername(u.Username)
u.Password = us.EncryptPassword(u.Password)
res := global.DB.Create(u).Error
@@ -343,13 +354,10 @@ func (us *UserService) RegisterByOauth(oauthUser *model.OauthUser, op string) (e
// GenerateUsernameByOauth 生成用户名
func (us *UserService) GenerateUsernameByOauth(name string) string {
u := &model.User{}
global.DB.Where("username = ?", name).First(u)
if u.Id == 0 {
return name
for us.IsUsernameExists(name) {
name += strconv.Itoa(rand.Intn(10)) // Append a random digit (0-9)
}
name = name + strconv.FormatInt(rand.Int63n(10), 10)
return us.GenerateUsernameByOauth(name)
return name
}
// UserThirdsByUserId
@@ -394,15 +402,18 @@ func (us *UserService) IsPasswordEmptyByUser(u *model.User) bool {
return us.IsPasswordEmptyById(u.Id)
}
// Register 注册
// Register 注册, 如果用户名已存在则返回nil
func (us *UserService) Register(username string, email string, password string) *model.User {
u := &model.User{
Username: username,
Email: email,
Password: us.EncryptPassword(password),
Password: password,
GroupId: 1,
}
global.DB.Create(u)
err := us.Create(u)
if err != nil {
return nil
}
return u
}
@@ -451,8 +462,17 @@ func (us *UserService) getAdminUserCount() int64 {
return count
}
// UserTokenExpireTimestamp 生成用户token过期时间
func (us *UserService) UserTokenExpireTimestamp() int64 {
exp := global.Config.App.TokenExpire
if exp == 0 {
exp = 3600 * 24 * 7
}
return time.Now().Add(time.Second * time.Duration(exp)).Unix()
}
func (us *UserService) RefreshAccessToken(ut *model.UserToken) {
ut.ExpiredAt = time.Now().Add(time.Hour * 24 * 7).Unix()
ut.ExpiredAt = us.UserTokenExpireTimestamp()
global.DB.Model(ut).Update("expired_at", ut.ExpiredAt)
}
func (us *UserService) AutoRefreshAccessToken(ut *model.UserToken) {
@@ -468,3 +488,11 @@ func (us *UserService) BatchDeleteUserToken(ids []uint) error {
func (us *UserService) VerifyJWT(token string) (uint, error) {
return global.Jwt.ParseToken(token)
}
// IsUsernameExists 判断用户名是否存在, it will check the internal database and LDAP(if enabled)
func (us *UserService) IsUsernameExists(username string) bool {
u := &model.User{}
global.DB.Where("username = ?", username).First(u)
existsInLdap := AllService.LdapService.IsUsernameExists(username)
return u.Id != 0 || existsInLdap
}