mirror of
https://github.com/criyle/go-sandbox.git
synced 2025-11-04 14:49:53 +08:00
refactor some package and add some documentation
This commit is contained in:
parent
a122ceed5a
commit
23c02267e6
@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
// getConf return file access check set, syscall counter, allow and traced syscall arrays and new args
|
||||
func getConf(pType, workPath string, args, addRead, addWrite []string, allowProc bool) (*fileSets, syscallCounter, []string, []string, []string) {
|
||||
var (
|
||||
fs = newFileSets()
|
||||
sc = newSyscallCounter()
|
||||
allow = append([]string{}, defaultSyscallAllows...)
|
||||
trace = append([]string{}, defaultSyscallTraces...)
|
||||
)
|
||||
|
||||
fs.Readable.AddRange(defaultReadableFiles)
|
||||
fs.Writable.AddRange(defaultWritableFiles)
|
||||
fs.addFilePermission(args[0], filePermRead)
|
||||
fs.addFilePermission(workPath, filePermRead)
|
||||
|
||||
fs.Readable.AddRange(addRead)
|
||||
fs.Writable.AddRange(addWrite)
|
||||
|
||||
if c, o := runprogramConfig[pType]; o {
|
||||
allow = append(allow, c.Syscall.ExtraAllow...)
|
||||
trace = append(trace, c.Syscall.ExtraBan...)
|
||||
sc.addRange(c.Syscall.ExtraCount)
|
||||
fs.Readable.AddRange(c.FileAccess.ExtraRead)
|
||||
fs.Writable.AddRange(c.FileAccess.ExtraWrite)
|
||||
fs.Statable.AddRange(c.FileAccess.ExtraStat)
|
||||
fs.SoftBan.AddRange(c.FileAccess.ExtraBan)
|
||||
args = append(c.RunCommand, args...)
|
||||
}
|
||||
if allowProc {
|
||||
allow = append(allow, defaultProcSyscalls...)
|
||||
}
|
||||
allow, trace = cleanTrace(allow, trace)
|
||||
|
||||
return fs, sc, allow, trace, args
|
||||
}
|
||||
|
||||
func keySetToSlice(m map[string]bool) []string {
|
||||
rt := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
rt = append(rt, k)
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
func cleanTrace(allow, trace []string) ([]string, []string) {
|
||||
// make sure allow, trace no duplicate
|
||||
traceMap := make(map[string]bool)
|
||||
for _, s := range trace {
|
||||
traceMap[s] = true
|
||||
}
|
||||
allowMap := make(map[string]bool)
|
||||
for _, s := range allow {
|
||||
if !traceMap[s] {
|
||||
allowMap[s] = true
|
||||
}
|
||||
}
|
||||
return keySetToSlice(allowMap), keySetToSlice(traceMap)
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/criyle/go-judger/runprogram"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
fs *fileSets
|
||||
sc syscallCounter
|
||||
showDetails bool
|
||||
}
|
||||
|
||||
func (h *handler) print(v ...interface{}) {
|
||||
if h.showDetails {
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) onDgsFileDetect(name string) runprogram.TraceAction {
|
||||
if h.fs.isSoftBanFile(name) {
|
||||
return runprogram.TraceBan
|
||||
}
|
||||
h.print("Dangerous fileopen: ", name)
|
||||
return runprogram.TraceKill
|
||||
}
|
||||
|
||||
func (h *handler) CheckRead(fn string) runprogram.TraceAction {
|
||||
if !h.fs.isReadableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
func (h *handler) CheckWrite(fn string) runprogram.TraceAction {
|
||||
if !h.fs.isWritableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
func (h *handler) CheckStat(fn string) runprogram.TraceAction {
|
||||
if !h.fs.isStatableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
func (h *handler) CheckSyscall(syscallName string) runprogram.TraceAction {
|
||||
// if it is traced, then try to count syscall
|
||||
if inside, allow := h.sc.check(syscallName); inside {
|
||||
if allow {
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
return runprogram.TraceKill
|
||||
}
|
||||
// if it is traced but not counted, it should be soft banned
|
||||
return runprogram.TraceBan
|
||||
}
|
||||
@ -5,11 +5,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/criyle/go-judger/runconfig"
|
||||
"github.com/criyle/go-judger/runprogram"
|
||||
"github.com/criyle/go-judger/tracer"
|
||||
)
|
||||
|
||||
// TODO: syscall handle, file access checker
|
||||
func printUsage() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <args>\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
addReadable, addWritable, addRawReadable, addRawWritable arrayFlags
|
||||
@ -19,6 +25,7 @@ func main() {
|
||||
inputFileName, outputFileName, errorFileName, workPath string
|
||||
)
|
||||
|
||||
flag.Usage = printUsage
|
||||
flag.UintVar(&timeLimit, "tl", 1, "Set time limit (in second)")
|
||||
flag.UintVar(&realTimeLimit, "rtl", 0, "Set real time limit (in second)")
|
||||
flag.UintVar(&memoryLimit, "ml", 256, "Set memory limit (in mb)")
|
||||
@ -37,12 +44,18 @@ func main() {
|
||||
flag.BoolVar(&allowProc, "allow-proc", false, "Allow fork, exec... etc.")
|
||||
flag.Var(&addRawReadable, "add-readable-raw", "Add a readable file (don't transform to its real path)")
|
||||
flag.Var(&addRawWritable, "add-writable-raw", "Add a writable file (don't transform to its real path)")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
addRead := getExtraSet(addReadable, addRawReadable)
|
||||
addWrite := getExtraSet(addWritable, addRawWritable)
|
||||
if len(args) == 0 {
|
||||
printUsage()
|
||||
}
|
||||
|
||||
println := func(v ...interface{}) {
|
||||
if showDetails {
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
}
|
||||
}
|
||||
|
||||
if realTimeLimit < timeLimit {
|
||||
realTimeLimit = timeLimit + 2
|
||||
@ -54,13 +67,9 @@ func main() {
|
||||
workPath, _ = os.Getwd()
|
||||
}
|
||||
|
||||
fs, sc, allow, trace, args := getConf(pType, workPath, args, addRead, addWrite, allowProc)
|
||||
|
||||
println := func(v ...interface{}) {
|
||||
if showDetails {
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
}
|
||||
}
|
||||
addRead := runconfig.GetExtraSet(addReadable, addRawReadable)
|
||||
addWrite := runconfig.GetExtraSet(addWritable, addRawWritable)
|
||||
h := runconfig.GetConf(pType, workPath, args, addRead, addWrite, allowProc, showDetails)
|
||||
|
||||
// open input / output / err files
|
||||
files, err := prepareFiles(inputFileName, outputFileName, errorFileName)
|
||||
@ -81,7 +90,7 @@ func main() {
|
||||
}
|
||||
|
||||
runner := &runprogram.RunProgram{
|
||||
Args: args,
|
||||
Args: h.Args,
|
||||
Env: []string{"PATH=/"},
|
||||
WorkDir: workPath,
|
||||
RLimits: runprogram.RLimits{
|
||||
@ -96,11 +105,11 @@ func main() {
|
||||
MemoryLimit: memoryLimit << 10,
|
||||
},
|
||||
Files: fds,
|
||||
SyscallAllowed: allow,
|
||||
SyscallTraced: trace,
|
||||
SyscallAllowed: h.SyscallAllow,
|
||||
SyscallTraced: h.SyscallTrace,
|
||||
ShowDetails: showDetails,
|
||||
Unsafe: unsafe,
|
||||
Handler: &handler{fs, sc, showDetails},
|
||||
Handler: h,
|
||||
}
|
||||
|
||||
var f *os.File
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
package main
|
||||
|
||||
type syscallCounter map[string]int
|
||||
|
||||
func newSyscallCounter() syscallCounter {
|
||||
return syscallCounter(make(map[string]int))
|
||||
}
|
||||
|
||||
func (s syscallCounter) add(name string, count int) {
|
||||
s[name] = count
|
||||
}
|
||||
|
||||
func (s syscallCounter) addRange(m map[string]int) {
|
||||
for k, v := range m {
|
||||
s[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// check return inside, allow
|
||||
func (s syscallCounter) check(name string) (bool, bool) {
|
||||
n, o := s[name]
|
||||
if o {
|
||||
s[name] = n - 1
|
||||
if n <= 1 {
|
||||
return true, false
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package runconfig
|
||||
|
||||
// This file includes configs for the run program settings
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package runconfig
|
||||
|
||||
// This file includes configs for the run program settings
|
||||
|
||||
66
runconfig/config_loader.go
Normal file
66
runconfig/config_loader.go
Normal file
@ -0,0 +1,66 @@
|
||||
package runconfig
|
||||
|
||||
// GetConf return file access check set, syscall counter, allow and traced syscall arrays and new args
|
||||
func GetConf(pType, workPath string, args, addRead, addWrite []string, allowProc, showDetails bool) *Handler {
|
||||
var (
|
||||
fs = NewFileSets()
|
||||
sc = NewSyscallCounter()
|
||||
allow = append([]string{}, defaultSyscallAllows...)
|
||||
trace = append([]string{}, defaultSyscallTraces...)
|
||||
)
|
||||
|
||||
fs.Readable.AddRange(defaultReadableFiles, workPath)
|
||||
fs.Writable.AddRange(defaultWritableFiles, workPath)
|
||||
fs.AddFilePermission(args[0], FilePermRead)
|
||||
fs.AddFilePermission(workPath, FilePermRead)
|
||||
|
||||
fs.Readable.AddRange(addRead, workPath)
|
||||
fs.Writable.AddRange(addWrite, workPath)
|
||||
|
||||
if c, o := runprogramConfig[pType]; o {
|
||||
allow = append(allow, c.Syscall.ExtraAllow...)
|
||||
trace = append(trace, c.Syscall.ExtraBan...)
|
||||
sc.AddRange(c.Syscall.ExtraCount)
|
||||
fs.Readable.AddRange(c.FileAccess.ExtraRead, workPath)
|
||||
fs.Writable.AddRange(c.FileAccess.ExtraWrite, workPath)
|
||||
fs.Statable.AddRange(c.FileAccess.ExtraStat, workPath)
|
||||
fs.SoftBan.AddRange(c.FileAccess.ExtraBan, workPath)
|
||||
args = append(c.RunCommand, args...)
|
||||
}
|
||||
if allowProc {
|
||||
allow = append(allow, defaultProcSyscalls...)
|
||||
}
|
||||
allow, trace = cleanTrace(allow, trace)
|
||||
|
||||
return &Handler{
|
||||
SyscallAllow: allow,
|
||||
SyscallTrace: trace,
|
||||
Args: args,
|
||||
FileSet: fs,
|
||||
SyscallCounter: sc,
|
||||
ShowDetails: showDetails,
|
||||
}
|
||||
}
|
||||
|
||||
func keySetToSlice(m map[string]bool) []string {
|
||||
rt := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
rt = append(rt, k)
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
func cleanTrace(allow, trace []string) ([]string, []string) {
|
||||
// make sure allow, trace no duplicate
|
||||
traceMap := make(map[string]bool)
|
||||
for _, s := range trace {
|
||||
traceMap[s] = true
|
||||
}
|
||||
allowMap := make(map[string]bool)
|
||||
for _, s := range allow {
|
||||
if !traceMap[s] {
|
||||
allowMap[s] = true
|
||||
}
|
||||
}
|
||||
return keySetToSlice(allowMap), keySetToSlice(traceMap)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package runconfig
|
||||
|
||||
// ProgramConfig defines the extra config apply to program type
|
||||
type ProgramConfig struct {
|
||||
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package runconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -8,26 +8,29 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fileSet stores the file permissions
|
||||
type fileSet struct {
|
||||
// FileSet stores the file permissions in the hierarchical set
|
||||
type FileSet struct {
|
||||
Set map[string]bool
|
||||
SystemRoot bool
|
||||
}
|
||||
|
||||
type filePerm int
|
||||
// FilePerm stores the permission apply to the file
|
||||
type FilePerm int
|
||||
|
||||
// FilePermWrite / Read / Stat are permissions
|
||||
const (
|
||||
filePermWrite = iota + 1
|
||||
filePermRead
|
||||
filePermStat
|
||||
FilePermWrite = iota + 1
|
||||
FilePermRead
|
||||
FilePermStat
|
||||
)
|
||||
|
||||
func newFileSet() fileSet {
|
||||
return fileSet{make(map[string]bool), false}
|
||||
// NewFileSet creates the new file set
|
||||
func NewFileSet() FileSet {
|
||||
return FileSet{make(map[string]bool), false}
|
||||
}
|
||||
|
||||
// IsInSetSmart same from uoj-judger
|
||||
func (s *fileSet) IsInSetSmart(name string) bool {
|
||||
func (s *FileSet) IsInSetSmart(name string) bool {
|
||||
if s.Set[name] {
|
||||
return true
|
||||
}
|
||||
@ -54,46 +57,60 @@ func (s *fileSet) IsInSetSmart(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *fileSet) Add(name string) {
|
||||
// Add adds a single file path into the FileSet
|
||||
func (s *FileSet) Add(name string) {
|
||||
s.Set[name] = true
|
||||
}
|
||||
|
||||
func (s *fileSet) AddRange(names []string) {
|
||||
// AddRange adds multiple files into the FileSet
|
||||
// If path is relative path, add according to the workPath
|
||||
func (s *FileSet) AddRange(names []string, workPath string) {
|
||||
for _, n := range names {
|
||||
s.Set[n] = true
|
||||
if filepath.IsAbs(n) {
|
||||
s.Set[n] = true
|
||||
} else {
|
||||
s.Set[filepath.Join(workPath, n)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fileSets struct {
|
||||
Writable, Readable, Statable, SoftBan fileSet
|
||||
// FileSets agregates multiple permissions including write / read / stat / soft ban
|
||||
type FileSets struct {
|
||||
Writable, Readable, Statable, SoftBan FileSet
|
||||
}
|
||||
|
||||
func newFileSets() *fileSets {
|
||||
return &fileSets{newFileSet(), newFileSet(), newFileSet(), newFileSet()}
|
||||
// NewFileSets creates new FileSets struct
|
||||
func NewFileSets() *FileSets {
|
||||
return &FileSets{NewFileSet(), NewFileSet(), NewFileSet(), NewFileSet()}
|
||||
}
|
||||
|
||||
func (s *fileSets) isWritableFile(name string) bool {
|
||||
// IsWritableFile determines whether the file path inside the write set
|
||||
func (s *FileSets) IsWritableFile(name string) bool {
|
||||
return s.Writable.IsInSetSmart(name) || s.Writable.IsInSetSmart(realPath(name))
|
||||
}
|
||||
|
||||
func (s *fileSets) isReadableFile(name string) bool {
|
||||
return s.isWritableFile(name) || s.Readable.IsInSetSmart(name) || s.Readable.IsInSetSmart(realPath(name))
|
||||
// IsReadableFile determines whether the file path inside the read / write set
|
||||
func (s *FileSets) IsReadableFile(name string) bool {
|
||||
return s.IsWritableFile(name) || s.Readable.IsInSetSmart(name) || s.Readable.IsInSetSmart(realPath(name))
|
||||
}
|
||||
|
||||
func (s *fileSets) isStatableFile(name string) bool {
|
||||
return s.isReadableFile(name) || s.Statable.IsInSetSmart(name) || s.Statable.IsInSetSmart(realPath(name))
|
||||
// IsStatableFile determines whether the file path inside the stat / read / write set
|
||||
func (s *FileSets) IsStatableFile(name string) bool {
|
||||
return s.IsReadableFile(name) || s.Statable.IsInSetSmart(name) || s.Statable.IsInSetSmart(realPath(name))
|
||||
}
|
||||
|
||||
func (s *fileSets) isSoftBanFile(name string) bool {
|
||||
// IsSoftBanFile determines whether the file path inside the softban set
|
||||
func (s *FileSets) IsSoftBanFile(name string) bool {
|
||||
return s.SoftBan.IsInSetSmart(name) || s.SoftBan.IsInSetSmart(realPath(name))
|
||||
}
|
||||
|
||||
func (s *fileSets) addFilePermission(name string, mode filePerm) {
|
||||
if mode == filePermWrite {
|
||||
// AddFilePermission adds the file into fileSets according to the given permission
|
||||
func (s *FileSets) AddFilePermission(name string, mode FilePerm) {
|
||||
if mode == FilePermWrite {
|
||||
s.Writable.Add(name)
|
||||
} else if mode == filePermRead {
|
||||
} else if mode == FilePermRead {
|
||||
s.Readable.Add(name)
|
||||
} else if mode == filePermStat {
|
||||
} else if mode == FilePermStat {
|
||||
s.Statable.Add(name)
|
||||
}
|
||||
for name = dirname(name); name != ""; name = dirname(name) {
|
||||
@ -101,6 +118,16 @@ func (s *fileSets) addFilePermission(name string, mode filePerm) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetExtraSet evaluates the concated file set according to real path or raw path
|
||||
func GetExtraSet(extra, raw []string) []string {
|
||||
rt := make([]string, 0, len(extra)+len(raw))
|
||||
rt = append(rt, raw...)
|
||||
for _, v := range extra {
|
||||
rt = append(rt, realPath(v))
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
||||
// basename return path with last "/"
|
||||
func basename(path string) string {
|
||||
if p := strings.LastIndex(path, "/"); p >= 0 {
|
||||
@ -147,12 +174,3 @@ func realPath(p string) string {
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func getExtraSet(extra, raw []string) []string {
|
||||
rt := make([]string, 0, len(extra)+len(raw))
|
||||
rt = append(rt, raw...)
|
||||
for _, v := range extra {
|
||||
rt = append(rt, realPath(v))
|
||||
}
|
||||
return rt
|
||||
}
|
||||
72
runconfig/handle.go
Normal file
72
runconfig/handle.go
Normal file
@ -0,0 +1,72 @@
|
||||
package runconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/criyle/go-judger/runprogram"
|
||||
)
|
||||
|
||||
// Handler defines file access restricted handler to call the runprogram
|
||||
// safe runner
|
||||
type Handler struct {
|
||||
SyscallAllow, SyscallTrace, Args []string
|
||||
FileSet *FileSets
|
||||
SyscallCounter SyscallCounter
|
||||
ShowDetails bool
|
||||
}
|
||||
|
||||
// CheckRead checks whether the file have read permission
|
||||
func (h *Handler) CheckRead(fn string) runprogram.TraceAction {
|
||||
if !h.FileSet.IsReadableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
// CheckWrite checks whether the file have write permission
|
||||
func (h *Handler) CheckWrite(fn string) runprogram.TraceAction {
|
||||
if !h.FileSet.IsWritableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
// CheckStat checks whether the file have stat permission
|
||||
func (h *Handler) CheckStat(fn string) runprogram.TraceAction {
|
||||
if !h.FileSet.IsStatableFile(fn) {
|
||||
return h.onDgsFileDetect(fn)
|
||||
}
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
|
||||
// CheckSyscall checks syscalls other than allowed and traced agianst the
|
||||
// SyscallCounter
|
||||
func (h *Handler) CheckSyscall(syscallName string) runprogram.TraceAction {
|
||||
// if it is traced, then try to count syscall
|
||||
if inside, allow := h.SyscallCounter.Check(syscallName); inside {
|
||||
if allow {
|
||||
return runprogram.TraceAllow
|
||||
}
|
||||
return runprogram.TraceKill
|
||||
}
|
||||
// if it is traced but not counted, it should be soft banned
|
||||
return runprogram.TraceBan
|
||||
}
|
||||
|
||||
// onDgsFileDetect soft ban file if in soft ban set
|
||||
// otherwise stops the trace process
|
||||
func (h *Handler) onDgsFileDetect(name string) runprogram.TraceAction {
|
||||
if h.FileSet.IsSoftBanFile(name) {
|
||||
return runprogram.TraceBan
|
||||
}
|
||||
h.print("Dangerous fileopen: ", name)
|
||||
return runprogram.TraceKill
|
||||
}
|
||||
|
||||
// print is used to print debug information
|
||||
func (h *Handler) print(v ...interface{}) {
|
||||
if h.ShowDetails {
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
}
|
||||
}
|
||||
34
runconfig/syscall_counter.go
Normal file
34
runconfig/syscall_counter.go
Normal file
@ -0,0 +1,34 @@
|
||||
package runconfig
|
||||
|
||||
// SyscallCounter defines a count-down for each each syscall occurs
|
||||
type SyscallCounter map[string]int
|
||||
|
||||
// NewSyscallCounter creates a new SyscallCounter
|
||||
func NewSyscallCounter() SyscallCounter {
|
||||
return SyscallCounter(make(map[string]int))
|
||||
}
|
||||
|
||||
// Add adds single counter to SyscallCounter
|
||||
func (s SyscallCounter) Add(name string, count int) {
|
||||
s[name] = count
|
||||
}
|
||||
|
||||
// AddRange add multiple counter to SyscallCounter
|
||||
func (s SyscallCounter) AddRange(m map[string]int) {
|
||||
for k, v := range m {
|
||||
s[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Check return inside, allow
|
||||
func (s SyscallCounter) Check(name string) (bool, bool) {
|
||||
n, o := s[name]
|
||||
if o {
|
||||
s[name] = n - 1
|
||||
if n <= 1 {
|
||||
return true, false
|
||||
}
|
||||
return true, true
|
||||
}
|
||||
return false, true
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user