mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-21 16:54:29 +00:00
lua: standardize hooks and host APIs on PascalCase
This commit is contained in:
+8
-8
@@ -18,12 +18,12 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
for i, client := range s.clients {
|
for i, client := range s.clients {
|
||||||
server := L.NewTable()
|
server := L.NewTable()
|
||||||
|
|
||||||
server.RawSetString("id", lua.LString(client.id))
|
server.RawSetString("ID", lua.LString(client.id))
|
||||||
|
|
||||||
server.RawSetString("query", L.NewFunction(func(L *lua.LState) int {
|
server.RawSetString("Query", L.NewFunction(func(L *lua.LState) int {
|
||||||
domain, ok := L.Get(2).(lua.LString)
|
domain, ok := L.Get(2).(lua.LString)
|
||||||
if !ok {
|
if !ok {
|
||||||
L.RaiseError("server:query requires a domain")
|
L.RaiseError("server:Query requires a domain")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
option := featureDNS.IPOption{
|
option := featureDNS.IPOption{
|
||||||
@@ -33,7 +33,7 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
}
|
}
|
||||||
ctx := L.Context()
|
ctx := L.Context()
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
L.RaiseError("server:query requires an active DNS query")
|
L.RaiseError("server:Query requires an active DNS query")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
var ips []net.IP
|
var ips []net.IP
|
||||||
@@ -60,13 +60,13 @@ func (s *DNS) RegisterLua(L *lua.LState) {
|
|||||||
servers.RawSetInt(i+1, server)
|
servers.RawSetInt(i+1, server)
|
||||||
}
|
}
|
||||||
module := L.NewTable()
|
module := L.NewTable()
|
||||||
module.RawSetString("servers", servers)
|
module.RawSetString("Servers", servers)
|
||||||
L.Push(module)
|
L.Push(module)
|
||||||
return 1
|
return 1
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CallLuaHook invokes handleDNSQuery in the supplied state.
|
// CallLuaHook invokes HandleDNSQuery in the supplied state.
|
||||||
// Returned slices and IP bytes may share storage with DNS caches or matcher inputs.
|
// Returned slices and IP bytes may share storage with DNS caches or matcher inputs.
|
||||||
func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, option featureDNS.IPOption) ([]net.IP, uint32, error) {
|
||||||
previous := L.Context()
|
previous := L.Context()
|
||||||
@@ -78,9 +78,9 @@ func (s *DNS) CallLuaHook(L *lua.LState, ctx context.Context, domain string, opt
|
|||||||
L.SetContext(previous)
|
L.SetContext(previous)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fn := L.GetGlobal("handleDNSQuery")
|
fn := L.GetGlobal("HandleDNSQuery")
|
||||||
if fn.Type() != lua.LTFunction {
|
if fn.Type() != lua.LTFunction {
|
||||||
return nil, 0, errors.New("DNS script must define handleDNSQuery(domain, ipv4, ipv6, fake)")
|
return nil, 0, errors.New("DNS script must define HandleDNSQuery(domain, ipv4, ipv6, fake)")
|
||||||
}
|
}
|
||||||
if err := L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true},
|
if err := L.CallByParam(lua.P{Fn: fn, NRet: 3, Protect: true},
|
||||||
lua.LString(strings.ToLower(domain)), lua.LBool(option.IPv4Enable),
|
lua.LString(strings.ToLower(domain)), lua.LBool(option.IPv4Enable),
|
||||||
|
|||||||
+13
-13
@@ -80,7 +80,7 @@ func TestReadLuaDNSResultValidation(t *testing.T) {
|
|||||||
func TestCallLuaHookCancellation(t *testing.T) {
|
func TestCallLuaHookCancellation(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
defer L.Close()
|
defer L.Close()
|
||||||
if err := L.DoString(`function handleDNSQuery(domain, ipv4, ipv6, fake) while true do end end`); err != nil {
|
if err := L.DoString(`function HandleDNSQuery(domain, ipv4, ipv6, fake) while true do end end`); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
@@ -101,7 +101,7 @@ func TestCallLuaHookNormalizesDomain(t *testing.T) {
|
|||||||
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
||||||
L.SetGlobal("ips", addresses)
|
L.SetGlobal("ips", addresses)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
assert(domain == "example.com")
|
assert(domain == "example.com")
|
||||||
assert(ipv4 and not ipv6 and not fake)
|
assert(ipv4 and not ipv6 and not fake)
|
||||||
return ips, 60, nil
|
return ips, 60, nil
|
||||||
@@ -130,7 +130,7 @@ func TestCallLuaHookRestoresState(t *testing.T) {
|
|||||||
addresses := L.NewUserData()
|
addresses := L.NewUserData()
|
||||||
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
addresses.Value = []net.IP{net.ParseIP("127.0.0.1")}
|
||||||
L.SetGlobal("ips", addresses)
|
L.SetGlobal("ips", addresses)
|
||||||
if err := L.DoString("function handleDNSQuery() " + tc.body + " end"); err != nil {
|
if err := L.DoString("function HandleDNSQuery() " + tc.body + " end"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
previous, cancel := context.WithCancel(context.Background())
|
previous, cancel := context.WithCancel(context.Background())
|
||||||
@@ -157,13 +157,13 @@ func TestLuaDNSServerQuery(t *testing.T) {
|
|||||||
server := &DNS{clients: []*Client{{server: &benchmarkLuaNameServer{ips: ips}, ipOption: &option, timeoutMs: time.Second}}}
|
server := &DNS{clients: []*Client{{server: &benchmarkLuaNameServer{ips: ips}, ipOption: &option, timeoutMs: time.Second}}}
|
||||||
server.RegisterLua(L)
|
server.RegisterLua(L)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
local server = require("xray.dns").servers[1]
|
local server = require("xray.dns").Servers[1]
|
||||||
local matcher = require("xray.geodata").ipMatcher("127.0.0.0/8")
|
local matcher = require("xray.geodata").IPMatcher("127.0.0.0/8")
|
||||||
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
local ips, ttl, err = server:query(domain, ipv4, ipv6, fake)
|
local ips, ttl, err = server:Query(domain, ipv4, ipv6, fake)
|
||||||
assert(type(ips) == "userdata" and not err)
|
assert(type(ips) == "userdata" and not err)
|
||||||
assert(matcher:anyMatch(ips))
|
assert(matcher:AnyMatch(ips))
|
||||||
local matched = matcher:filterIPs(ips)
|
local matched = matcher:FilterIPs(ips)
|
||||||
return matched, ttl, err
|
return matched, ttl, err
|
||||||
end
|
end
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
@@ -185,7 +185,7 @@ func (s *benchmarkLuaNameServer) QueryIP(context.Context, string, featureDNS.IPO
|
|||||||
return s.ips, 60, nil
|
return s.ips, 60, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkLuaDNSHookCall isolates a preloaded Lua hook and its server:query bridge.
|
// BenchmarkLuaDNSHookCall isolates a preloaded Lua hook and its server:Query bridge.
|
||||||
// The direct case measures the same DNS client without Lua.
|
// The direct case measures the same DNS client without Lua.
|
||||||
func BenchmarkLuaDNSHookCall(b *testing.B) {
|
func BenchmarkLuaDNSHookCall(b *testing.B) {
|
||||||
option := featureDNS.IPOption{IPv4Enable: true}
|
option := featureDNS.IPOption{IPv4Enable: true}
|
||||||
@@ -197,9 +197,9 @@ func BenchmarkLuaDNSHookCall(b *testing.B) {
|
|||||||
defer L.Close()
|
defer L.Close()
|
||||||
server.RegisterLua(L)
|
server.RegisterLua(L)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
local server = require("xray.dns").servers[1]
|
local server = require("xray.dns").Servers[1]
|
||||||
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
return server:query(domain, ipv4, ipv6, fake)
|
return server:Query(domain, ipv4, ipv6, fake)
|
||||||
end
|
end
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
|
|||||||
+2
-2
@@ -37,9 +37,9 @@ func newScriptEngine(path string, server *DNS) (*scriptEngine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if L.GetGlobal("handleDNSQuery").Type() != lua.LTFunction {
|
if L.GetGlobal("HandleDNSQuery").Type() != lua.LTFunction {
|
||||||
L.Close()
|
L.Close()
|
||||||
return nil, errors.New("DNS script must define handleDNSQuery(domain, ipv4, ipv6, fake)")
|
return nil, errors.New("DNS script must define HandleDNSQuery(domain, ipv4, ipv6, fake)")
|
||||||
}
|
}
|
||||||
return L, nil
|
return L, nil
|
||||||
})
|
})
|
||||||
|
|||||||
+14
-14
@@ -37,21 +37,21 @@ func (s *geoIPScriptNameServer) QueryIP(ctx context.Context, domain string, _ fe
|
|||||||
func TestDNSScriptGeoIPFallback(t *testing.T) {
|
func TestDNSScriptGeoIPFallback(t *testing.T) {
|
||||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
script := `
|
script := `
|
||||||
local servers = require("xray.dns").servers
|
local servers = require("xray.dns").Servers
|
||||||
local us_ips = require("xray.geodata").ipMatcher("geoip:us")
|
local us_ips = require("xray.geodata").IPMatcher("geoip:us")
|
||||||
|
|
||||||
local by_id = {}
|
local by_id = {}
|
||||||
for _, server in ipairs(servers) do
|
for _, server in ipairs(servers) do
|
||||||
by_id[server.id] = server
|
by_id[server.ID] = server
|
||||||
end
|
end
|
||||||
assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are required")
|
assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are required")
|
||||||
|
|
||||||
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
local ips, ttl, err = by_id.primary:query(domain, ipv4, ipv6, fake)
|
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
|
||||||
if not err and us_ips:anyMatch(ips) then
|
if not err and us_ips:AnyMatch(ips) then
|
||||||
return ips, ttl, nil
|
return ips, ttl, nil
|
||||||
end
|
end
|
||||||
return by_id.fallback:query(domain, ipv4, ipv6, fake)
|
return by_id.fallback:Query(domain, ipv4, ipv6, fake)
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
scriptPath := filepath.Join(t.TempDir(), "geoip_fallback.lua")
|
scriptPath := filepath.Join(t.TempDir(), "geoip_fallback.lua")
|
||||||
@@ -118,7 +118,7 @@ func TestDNSScriptRejectsInvalidStartup(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
script string
|
script string
|
||||||
}{
|
}{
|
||||||
{"syntax", "function handleDNSQuery("},
|
{"syntax", "function HandleDNSQuery("},
|
||||||
{"missing hook", "value = 1"},
|
{"missing hook", "value = 1"},
|
||||||
{"top-level error", `error("setup failed")`},
|
{"top-level error", `error("setup failed")`},
|
||||||
} {
|
} {
|
||||||
@@ -141,14 +141,14 @@ func TestDNSScriptRejectsInvalidStartup(t *testing.T) {
|
|||||||
func TestDNSScriptHookErrorAndFakeDNSOption(t *testing.T) {
|
func TestDNSScriptHookErrorAndFakeDNSOption(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "script.lua")
|
path := filepath.Join(t.TempDir(), "script.lua")
|
||||||
script := `
|
script := `
|
||||||
local server = require("xray.dns").servers[1]
|
local server = require("xray.dns").Servers[1]
|
||||||
local log = require("xray.log")
|
local log = require("xray.log")
|
||||||
log.info("DNS script loaded")
|
log.Info("DNS script loaded")
|
||||||
function handleDNSQuery(domain, ipv4, ipv6, fake)
|
function HandleDNSQuery(domain, ipv4, ipv6, fake)
|
||||||
log.debug("DNS query: ", domain)
|
log.Debug("DNS query: ", domain)
|
||||||
if domain == "bad.example" then error("script failure") end
|
if domain == "bad.example" then error("script failure") end
|
||||||
local ips, ttl, err = server:query(domain, ipv4, ipv6, fake)
|
local ips, ttl, err = server:Query(domain, ipv4, ipv6, fake)
|
||||||
if err then log.error("DNS failed: ", err) end
|
if err then log.Error("DNS failed: ", err) end
|
||||||
return ips, ttl, err
|
return ips, ttl, err
|
||||||
end
|
end
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func RegisterLua(L *lua.LState) {
|
|||||||
L.PreloadModule("xray.geodata", func(L *lua.LState) int {
|
L.PreloadModule("xray.geodata", func(L *lua.LState) int {
|
||||||
module := L.NewTable()
|
module := L.NewTable()
|
||||||
|
|
||||||
module.RawSetString("domainMatcher", L.NewFunction(func(L *lua.LState) int {
|
module.RawSetString("DomainMatcher", L.NewFunction(func(L *lua.LState) int {
|
||||||
parsed, err := ParseDomainRules(luaRules(L), Domain_Domain)
|
parsed, err := ParseDomainRules(luaRules(L), Domain_Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
L.RaiseError("%v", err)
|
L.RaiseError("%v", err)
|
||||||
@@ -25,7 +25,7 @@ func RegisterLua(L *lua.LState) {
|
|||||||
return 1
|
return 1
|
||||||
}))
|
}))
|
||||||
|
|
||||||
module.RawSetString("ipMatcher", L.NewFunction(func(L *lua.LState) int {
|
module.RawSetString("IPMatcher", L.NewFunction(func(L *lua.LState) int {
|
||||||
parsed, err := ParseIPRules(luaRules(L))
|
parsed, err := ParseIPRules(luaRules(L))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
L.RaiseError("%v", err)
|
L.RaiseError("%v", err)
|
||||||
|
|||||||
+13
-13
@@ -18,11 +18,11 @@ func TestLuaIPMatcher(t *testing.T) {
|
|||||||
ips.Value = []net.IP{ip.Value.(net.IP), net.ParseIP("8.8.8.8")}
|
ips.Value = []net.IP{ip.Value.(net.IP), net.ParseIP("8.8.8.8")}
|
||||||
L.SetGlobal("ips", ips)
|
L.SetGlobal("ips", ips)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
local matcher = require("xray.geodata").ipMatcher("127.0.0.0/8", "::1")
|
local matcher = require("xray.geodata").IPMatcher("127.0.0.0/8", "::1")
|
||||||
assert(matcher:match(ip))
|
assert(matcher:Match(ip))
|
||||||
assert(matcher:anyMatch(ips))
|
assert(matcher:AnyMatch(ips))
|
||||||
assert(not matcher:matches(ips))
|
assert(not matcher:Matches(ips))
|
||||||
local matched, unmatched = matcher:filterIPs(ips)
|
local matched, unmatched = matcher:FilterIPs(ips)
|
||||||
assert(type(matched) == "userdata" and type(unmatched) == "userdata")
|
assert(type(matched) == "userdata" and type(unmatched) == "userdata")
|
||||||
assert(#matched == 1 and #unmatched == 1)
|
assert(#matched == 1 and #unmatched == 1)
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
@@ -35,12 +35,12 @@ func TestLuaDomainMatcher(t *testing.T) {
|
|||||||
defer L.Close()
|
defer L.Close()
|
||||||
RegisterLua(L)
|
RegisterLua(L)
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
local matcher = require("xray.geodata").domainMatcher("example.com", "full:other.com")
|
local matcher = require("xray.geodata").DomainMatcher("example.com", "full:other.com")
|
||||||
assert(matcher:matchAny("example.com"))
|
assert(matcher:MatchAny("example.com"))
|
||||||
assert(matcher:matchAny("www.example.com"))
|
assert(matcher:MatchAny("www.example.com"))
|
||||||
assert(matcher:matchAny("other.com"))
|
assert(matcher:MatchAny("other.com"))
|
||||||
assert(not matcher:matchAny("www.other.com"))
|
assert(not matcher:MatchAny("www.other.com"))
|
||||||
assert(#(matcher:match("www.example.com")) == 1)
|
assert(#(matcher:Match("www.example.com")) == 1)
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -51,8 +51,8 @@ func TestLuaMatchersRejectInvalidRules(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
script string
|
script string
|
||||||
}{
|
}{
|
||||||
{"IP rule", `require("xray.geodata").ipMatcher("not-an-ip")`},
|
{"IP rule", `require("xray.geodata").IPMatcher("not-an-ip")`},
|
||||||
{"non-string domain rule", `require("xray.geodata").domainMatcher("example.com", true)`},
|
{"non-string domain rule", `require("xray.geodata").DomainMatcher("example.com", true)`},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
|
|||||||
+4
-4
@@ -12,10 +12,10 @@ func RegisterLua(L *lua.LState) {
|
|||||||
L.PreloadModule("xray.log", func(L *lua.LState) int {
|
L.PreloadModule("xray.log", func(L *lua.LState) int {
|
||||||
module := L.NewTable()
|
module := L.NewTable()
|
||||||
for name, severity := range map[string]Severity{
|
for name, severity := range map[string]Severity{
|
||||||
"debug": Severity_Debug,
|
"Debug": Severity_Debug,
|
||||||
"info": Severity_Info,
|
"Info": Severity_Info,
|
||||||
"warning": Severity_Warning,
|
"Warning": Severity_Warning,
|
||||||
"error": Severity_Error,
|
"Error": Severity_Error,
|
||||||
} {
|
} {
|
||||||
module.RawSetString(name, L.NewFunction(func(L *lua.LState) int {
|
module.RawSetString(name, L.NewFunction(func(L *lua.LState) int {
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
|||||||
@@ -36,19 +36,19 @@ func TestLuaLog(t *testing.T) {
|
|||||||
if err := os.WriteFile(path, []byte(`
|
if err := os.WriteFile(path, []byte(`
|
||||||
local log = require("xray.log")
|
local log = require("xray.log")
|
||||||
assert(log == require("xray.log"))
|
assert(log == require("xray.log"))
|
||||||
log.debug("query: ", "example.com")
|
log.Debug("query: ", "example.com")
|
||||||
log.info("count=", 42, ", enabled=", true, ", value=", nil)
|
log.Info("count=", 42, ", enabled=", true, ", value=", nil)
|
||||||
log.warning(setmetatable({}, {
|
log.Warning(setmetatable({}, {
|
||||||
__tostring = function() return "fallback" end
|
__tostring = function() return "fallback" end
|
||||||
}))
|
}))
|
||||||
assert(select("#", log.error("failed")) == 0)
|
assert(select("#", log.Error("failed")) == 0)
|
||||||
log.error("DNS failed: ", nativeError)
|
log.Error("DNS failed: ", nativeError)
|
||||||
log.warning(nativeError)
|
log.Warning(nativeError)
|
||||||
local ok, err = pcall(function() error("Lua failure", 0) end)
|
local ok, err = pcall(function() error("Lua failure", 0) end)
|
||||||
assert(not ok)
|
assert(not ok)
|
||||||
log.error(err)
|
log.Error(err)
|
||||||
function logHook()
|
function logHook()
|
||||||
log.info("hook")
|
log.Info("hook")
|
||||||
end
|
end
|
||||||
`), 0o600); err != nil {
|
`), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -58,7 +58,7 @@ func TestLuaLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
if err := L.DoString(`
|
if err := L.DoString(`
|
||||||
logHook()
|
logHook()
|
||||||
require("xray.log").info("anonymous")
|
require("xray.log").Info("anonymous")
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func TestDNSScriptConfig(t *testing.T) {
|
|||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
t.Setenv("xray.location.confdir", dir)
|
t.Setenv("xray.location.confdir", dir)
|
||||||
path := filepath.Join(dir, "lookup.lua")
|
path := filepath.Join(dir, "lookup.lua")
|
||||||
if err := os.WriteFile(path, []byte("function handleDNSQuery(domain, ipv4, ipv6, fake) end"), 0o600); err != nil {
|
if err := os.WriteFile(path, []byte("function HandleDNSQuery(domain, ipv4, ipv6, fake) end"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user