1 package selinux
2
3 import (
4 "bufio"
5 "bytes"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strconv"
11 "testing"
12 )
13
14 func TestSetFileLabel(t *testing.T) {
15 if !GetEnabled() {
16 t.Skip("SELinux not enabled, skipping.")
17 }
18
19 const (
20 tmpFile = "selinux_test"
21 tmpLink = "selinux_test_link"
22 con = "system_u:object_r:bin_t:s0:c1,c2"
23 con2 = "system_u:object_r:bin_t:s0:c3,c4"
24 )
25
26 _ = os.Remove(tmpFile)
27 out, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE, 0)
28 if err != nil {
29 t.Fatal(err)
30 }
31 out.Close()
32 defer os.Remove(tmpFile)
33
34 _ = os.Remove(tmpLink)
35 if err := os.Symlink(tmpFile, tmpLink); err != nil {
36 t.Fatal(err)
37 }
38 defer os.Remove(tmpLink)
39
40 if err := SetFileLabel(tmpLink, con); err != nil {
41 t.Fatalf("SetFileLabel failed: %s", err)
42 }
43 filelabel, err := FileLabel(tmpLink)
44 if err != nil {
45 t.Fatalf("FileLabel failed: %s", err)
46 }
47 if filelabel != con {
48 t.Fatalf("FileLabel failed, returned %s expected %s", filelabel, con)
49 }
50
51
52 linkLabel, err := LfileLabel(tmpLink)
53 if err != nil {
54 t.Fatalf("LfileLabel failed: %s", err)
55 }
56 if linkLabel == con {
57 t.Fatalf("Label on symlink should not be set, got: %q", linkLabel)
58 }
59
60
61 if err := LsetFileLabel(tmpLink, con2); err != nil {
62 t.Fatalf("LsetFileLabel failed: %s", err)
63 }
64 filelabel, err = FileLabel(tmpFile)
65 if err != nil {
66 t.Fatalf("FileLabel failed: %s", err)
67 }
68 if filelabel != con {
69 t.Fatalf("FileLabel was updated, returned %s expected %s", filelabel, con)
70 }
71
72 linkLabel, err = LfileLabel(tmpLink)
73 if err != nil {
74 t.Fatalf("LfileLabel failed: %s", err)
75 }
76 if linkLabel != con2 {
77 t.Fatalf("LfileLabel failed: returned %s expected %s", linkLabel, con2)
78 }
79 }
80
81 func TestKVMLabels(t *testing.T) {
82 if !GetEnabled() {
83 t.Skip("SELinux not enabled, skipping.")
84 }
85
86 plabel, flabel := KVMContainerLabels()
87 if plabel == "" {
88 t.Log("Failed to read kvm label")
89 }
90 t.Log(plabel)
91 t.Log(flabel)
92 if _, err := CanonicalizeContext(plabel); err != nil {
93 t.Fatal(err)
94 }
95 if _, err := CanonicalizeContext(flabel); err != nil {
96 t.Fatal(err)
97 }
98
99 ReleaseLabel(plabel)
100 }
101
102 func TestInitLabels(t *testing.T) {
103 if !GetEnabled() {
104 t.Skip("SELinux not enabled, skipping.")
105 }
106
107 plabel, flabel := InitContainerLabels()
108 if plabel == "" {
109 t.Log("Failed to read init label")
110 }
111 t.Log(plabel)
112 t.Log(flabel)
113 if _, err := CanonicalizeContext(plabel); err != nil {
114 t.Fatal(err)
115 }
116 if _, err := CanonicalizeContext(flabel); err != nil {
117 t.Fatal(err)
118 }
119 ReleaseLabel(plabel)
120 }
121
122 func BenchmarkContextGet(b *testing.B) {
123 ctx, err := NewContext("system_u:object_r:container_file_t:s0:c1022,c1023")
124 if err != nil {
125 b.Fatal(err)
126 }
127 str := ""
128 for i := 0; i < b.N; i++ {
129 str = ctx.get()
130 }
131 b.Log(str)
132 }
133
134 func TestSELinux(t *testing.T) {
135 if !GetEnabled() {
136 t.Skip("SELinux not enabled, skipping.")
137 }
138
139 var (
140 err error
141 plabel, flabel string
142 )
143
144 plabel, flabel = ContainerLabels()
145 t.Log(plabel)
146 t.Log(flabel)
147 plabel, flabel = ContainerLabels()
148 t.Log(plabel)
149 t.Log(flabel)
150 ReleaseLabel(plabel)
151
152 plabel, flabel = ContainerLabels()
153 t.Log(plabel)
154 t.Log(flabel)
155 ClearLabels()
156 t.Log("ClearLabels")
157 plabel, flabel = ContainerLabels()
158 t.Log(plabel)
159 t.Log(flabel)
160 ReleaseLabel(plabel)
161
162 pid := os.Getpid()
163 t.Logf("PID:%d MCS:%s\n", pid, intToMcs(pid, 1023))
164 err = SetFSCreateLabel("unconfined_u:unconfined_r:unconfined_t:s0")
165 if err == nil {
166 t.Log(FSCreateLabel())
167 } else {
168 t.Log("SetFSCreateLabel failed", err)
169 t.Fatal(err)
170 }
171 err = SetFSCreateLabel("")
172 if err == nil {
173 t.Log(FSCreateLabel())
174 } else {
175 t.Log("SetFSCreateLabel failed", err)
176 t.Fatal(err)
177 }
178 t.Log(PidLabel(1))
179 }
180
181 func TestSetEnforceMode(t *testing.T) {
182 if !GetEnabled() {
183 t.Skip("SELinux not enabled, skipping.")
184 }
185 if os.Geteuid() != 0 {
186 t.Skip("root required, skipping")
187 }
188
189 t.Log("Enforcing Mode:", EnforceMode())
190 mode := DefaultEnforceMode()
191 t.Log("Default Enforce Mode:", mode)
192 defer func() {
193 _ = SetEnforceMode(mode)
194 }()
195
196 if err := SetEnforceMode(Enforcing); err != nil {
197 t.Fatalf("setting selinux mode to enforcing failed: %v", err)
198 }
199 if err := SetEnforceMode(Permissive); err != nil {
200 t.Fatalf("setting selinux mode to permissive failed: %v", err)
201 }
202 }
203
204 func TestCanonicalizeContext(t *testing.T) {
205 if !GetEnabled() {
206 t.Skip("SELinux not enabled, skipping.")
207 }
208
209 con := "system_u:object_r:bin_t:s0:c1,c2,c3"
210 checkcon := "system_u:object_r:bin_t:s0:c1.c3"
211 newcon, err := CanonicalizeContext(con)
212 if err != nil {
213 t.Fatal(err)
214 }
215 if newcon != checkcon {
216 t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
217 }
218 con = "system_u:object_r:bin_t:s0:c5,c2"
219 checkcon = "system_u:object_r:bin_t:s0:c2,c5"
220 newcon, err = CanonicalizeContext(con)
221 if err != nil {
222 t.Fatal(err)
223 }
224 if newcon != checkcon {
225 t.Fatalf("CanonicalizeContext(%s) returned %s expected %s", con, newcon, checkcon)
226 }
227 }
228
229 func TestFindSELinuxfsInMountinfo(t *testing.T) {
230
231 const mountinfo = `18 62 0:17 / /sys rw,nosuid,nodev,noexec,relatime shared:6 - sysfs sysfs rw,seclabel
232 19 62 0:3 / /proc rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
233 20 62 0:5 / /dev rw,nosuid shared:2 - devtmpfs devtmpfs rw,seclabel,size=3995472k,nr_inodes=998868,mode=755
234 21 18 0:16 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:7 - securityfs securityfs rw
235 22 20 0:18 / /dev/shm rw,nosuid,nodev shared:3 - tmpfs tmpfs rw,seclabel
236 23 20 0:11 / /dev/pts rw,nosuid,noexec,relatime shared:4 - devpts devpts rw,seclabel,gid=5,mode=620,ptmxmode=000
237 24 62 0:19 / /run rw,nosuid,nodev shared:23 - tmpfs tmpfs rw,seclabel,mode=755
238 25 18 0:20 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:8 - tmpfs tmpfs ro,seclabel,mode=755
239 26 25 0:21 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:9 - cgroup cgroup rw,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
240 27 18 0:22 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:20 - pstore pstore rw
241 28 25 0:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,perf_event
242 29 25 0:24 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:11 - cgroup cgroup rw,devices
243 30 25 0:25 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:12 - cgroup cgroup rw,cpuacct,cpu
244 31 25 0:26 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,freezer
245 32 25 0:27 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,net_prio,net_cls
246 33 25 0:28 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset
247 34 25 0:29 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,memory
248 35 25 0:30 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids
249 36 25 0:31 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,hugetlb
250 37 25 0:32 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,blkio
251 59 18 0:33 / /sys/kernel/config rw,relatime shared:21 - configfs configfs rw
252 62 1 253:1 / / rw,relatime shared:1 - ext4 /dev/vda1 rw,seclabel,data=ordered
253 38 18 0:15 / /sys/fs/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
254 39 19 0:35 / /proc/sys/fs/binfmt_misc rw,relatime shared:24 - autofs systemd-1 rw,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=11601
255 40 20 0:36 / /dev/hugepages rw,relatime shared:25 - hugetlbfs hugetlbfs rw,seclabel
256 41 20 0:14 / /dev/mqueue rw,relatime shared:26 - mqueue mqueue rw,seclabel
257 42 18 0:6 / /sys/kernel/debug rw,relatime shared:27 - debugfs debugfs rw
258 112 62 253:1 /var/lib/docker/plugins /var/lib/docker/plugins rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
259 115 62 253:1 /var/lib/docker/overlay2 /var/lib/docker/overlay2 rw,relatime - ext4 /dev/vda1 rw,seclabel,data=ordered
260 118 62 7:0 / /root/mnt rw,relatime shared:66 - ext4 /dev/loop0 rw,seclabel,data=ordered
261 121 115 0:38 / /var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/merged rw,relatime - overlay overlay rw,seclabel,lowerdir=/var/lib/docker/overlay2/l/CPD4XI7UD4GGTGSJVPQSHWZKTK:/var/lib/docker/overlay2/l/NQKORR3IS7KNQDER35AZECLH4Z,upperdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/diff,workdir=/var/lib/docker/overlay2/8cdbabf81bc89b14ea54eaf418c1922068f06917fff57e184aa26541ff291073/work
262 125 62 0:39 / /var/lib/docker/containers/5e3fce422957c291a5b502c2cf33d512fc1fcac424e4113136c808360e5b7215/shm rw,nosuid,nodev,noexec,relatime shared:68 - tmpfs shm rw,seclabel,size=65536k
263 186 24 0:3 / /run/docker/netns/0a08e7496c6d rw,nosuid,nodev,noexec,relatime shared:5 - proc proc rw
264 130 62 0:15 / /root/chroot/selinux rw,relatime shared:22 - selinuxfs selinuxfs rw
265 109 24 0:37 / /run/user/0 rw,nosuid,nodev,relatime shared:62 - tmpfs tmpfs rw,seclabel,size=801032k,mode=700
266 `
267 s := bufio.NewScanner(bytes.NewBuffer([]byte(mountinfo)))
268 for _, expected := range []string{"/sys/fs/selinux", "/root/chroot/selinux", ""} {
269 mnt := findSELinuxfsMount(s)
270 t.Logf("found %q", mnt)
271 if mnt != expected {
272 t.Fatalf("expected %q, got %q", expected, mnt)
273 }
274 }
275 }
276
277 func TestSecurityCheckContext(t *testing.T) {
278 if !GetEnabled() {
279 t.Skip("SELinux not enabled, skipping.")
280 }
281
282
283 context, err := CurrentLabel()
284 if err != nil {
285 t.Fatalf("CurrentLabel() error: %v", err)
286 }
287 if context != "" {
288 t.Logf("SecurityCheckContext(%q)", context)
289 err = SecurityCheckContext(context)
290 if err != nil {
291 t.Errorf("SecurityCheckContext(%q) error: %v", context, err)
292 }
293 }
294
295 context = "not-syntactically-valid"
296 err = SecurityCheckContext(context)
297 if err == nil {
298 t.Errorf("SecurityCheckContext(%q) succeeded, expected to fail", context)
299 }
300 }
301
302 func TestClassIndex(t *testing.T) {
303 if !GetEnabled() {
304 t.Skip("SELinux not enabled, skipping.")
305 }
306
307 idx, err := ClassIndex("process")
308 if err != nil {
309 t.Errorf("Classindex error: %v", err)
310 }
311
312 if idx != 2 {
313 t.Errorf("ClassIndex unexpected answer %d, possibly not reference policy", idx)
314 }
315
316 _, err = ClassIndex("foobar")
317 if err == nil {
318 t.Errorf("ClassIndex(\"foobar\") succeeded, expected to fail:")
319 }
320 }
321
322 func TestComputeCreateContext(t *testing.T) {
323 if !GetEnabled() {
324 t.Skip("SELinux not enabled, skipping.")
325 }
326
327
328 init := "system_u:system_r:init_t:s0"
329 tmp := "system_u:object_r:tmp_t:s0"
330 file := "file"
331 t.Logf("ComputeCreateContext(%s, %s, %s)", init, tmp, file)
332 context, err := ComputeCreateContext(init, tmp, file)
333 if err != nil {
334 t.Errorf("ComputeCreateContext error: %v", err)
335 }
336 if context != "system_u:object_r:init_tmp_t:s0" {
337 t.Errorf("ComputeCreateContext unexpected answer %s, possibly not reference policy", context)
338 }
339
340 badcon := "badcon"
341 process := "process"
342
343 t.Logf("ComputeCreateContext(%s, %s, %s)", badcon, tmp, process)
344 _, err = ComputeCreateContext(badcon, tmp, process)
345 if err == nil {
346 t.Errorf("ComputeCreateContext(%s, %s, %s) succeeded, expected failure", badcon, tmp, process)
347 }
348 }
349
350 func TestGlbLub(t *testing.T) {
351 tests := []struct {
352 expectedErr error
353 sourceRange string
354 targetRange string
355 expectedRange string
356 }{
357 {
358 sourceRange: "s0:c0.c100-s10:c0.c150",
359 targetRange: "s5:c50.c100-s15:c0.c149",
360 expectedRange: "s5:c50.c100-s10:c0.c149",
361 },
362 {
363 sourceRange: "s5:c50.c100-s15:c0.c149",
364 targetRange: "s0:c0.c100-s10:c0.c150",
365 expectedRange: "s5:c50.c100-s10:c0.c149",
366 },
367 {
368 sourceRange: "s0:c0.c100-s10:c0.c150",
369 targetRange: "s0",
370 expectedRange: "s0",
371 },
372 {
373 sourceRange: "s6:c0.c1023",
374 targetRange: "s6:c0,c2,c11,c201.c429,c431.c511",
375 expectedRange: "s6:c0,c2,c11,c201.c429,c431.c511",
376 },
377 {
378 sourceRange: "s0-s15:c0.c1023",
379 targetRange: "s6:c0,c2,c11,c201.c429,c431.c511",
380 expectedRange: "s6-s6:c0,c2,c11,c201.c429,c431.c511",
381 },
382 {
383 sourceRange: "s0:c0.c100,c125,c140,c150-s10",
384 targetRange: "s4:c0.c50,c140",
385 expectedRange: "s4:c0.c50,c140-s4",
386 },
387 {
388 sourceRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
389 targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
390 expectedRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
391 },
392 {
393 sourceRange: "s5:c512.c540,c542,c543,c552.c1023-s5:c0.c550,c552.c1023",
394 targetRange: "s5:c512.c550,c553.c1023-s5:c0,c1,c4,c5,c6,c512.c550,c553.c1023",
395 expectedRange: "s5:c512.c540,c542,c543,c553.c1023-s5:c0,c1,c4.c6,c512.c550,c553.c1023",
396 },
397 {
398 sourceRange: "s5:c50.c100-s15:c0.c149",
399 targetRange: "s5:c512.c550,c552.c1023-s5:c0.c550,c552.c1023",
400 expectedRange: "s5-s5:c0.c149",
401 },
402 {
403 sourceRange: "s5-s15",
404 targetRange: "s6-s7",
405 expectedRange: "s6-s7",
406 },
407 {
408 sourceRange: "s5:c50.c100-s15:c0.c149",
409 targetRange: "s4-s4:c0.c1023",
410 expectedErr: ErrIncomparable,
411 },
412 {
413 sourceRange: "s4-s4:c0.c1023",
414 targetRange: "s5:c50.c100-s15:c0.c149",
415 expectedErr: ErrIncomparable,
416 },
417 {
418 sourceRange: "s4-s4:c0.c1023.c10000",
419 targetRange: "s5:c50.c100-s15:c0.c149",
420 expectedErr: strconv.ErrSyntax,
421 },
422 {
423 sourceRange: "s4-s4:c0.c1023.c10000-s4",
424 targetRange: "s5:c50.c100-s15:c0.c149-s5",
425 expectedErr: strconv.ErrSyntax,
426 },
427 {
428 sourceRange: "4-4",
429 targetRange: "s5:c50.c100-s15:c0.c149",
430 expectedErr: ErrLevelSyntax,
431 },
432 {
433 sourceRange: "t4-t4",
434 targetRange: "s5:c50.c100-s15:c0.c149",
435 expectedErr: ErrLevelSyntax,
436 },
437 {
438 sourceRange: "s5:x50.x100-s15:c0.c149",
439 targetRange: "s5:c50.c100-s15:c0.c149",
440 expectedErr: ErrLevelSyntax,
441 },
442 }
443
444 for _, tt := range tests {
445 got, err := CalculateGlbLub(tt.sourceRange, tt.targetRange)
446 if !errors.Is(err, tt.expectedErr) {
447
448
449
450 var numErr *strconv.NumError
451 if errors.As(err, &numErr) && numErr.Err == tt.expectedErr {
452 continue
453 }
454 t.Fatalf("want %q got %q: src: %q tgt: %q", tt.expectedErr, err, tt.sourceRange, tt.targetRange)
455 }
456
457 if got != tt.expectedRange {
458 t.Errorf("want %q got %q", tt.expectedRange, got)
459 }
460 }
461 }
462
463 func TestContextWithLevel(t *testing.T) {
464 want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh"
465
466 goodDefaultBuff := `
467 foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
468 staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
469 `
470
471 verifier := func(con string) error {
472 if con != want {
473 return fmt.Errorf("invalid context %s", con)
474 }
475
476 return nil
477 }
478
479 tests := []struct {
480 name, userBuff, defaultBuff string
481 }{
482 {
483 name: "match exists in user context file",
484 userBuff: `# COMMENT
485 foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
486
487 staff_r:staff_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
488 `,
489 defaultBuff: goodDefaultBuff,
490 },
491 {
492 name: "match exists in default context file, but not in user file",
493 userBuff: `# COMMENT
494 foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
495 fake_r:fake_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
496 `,
497 defaultBuff: goodDefaultBuff,
498 },
499 }
500
501 for _, tt := range tests {
502 t.Run(tt.name, func(t *testing.T) {
503 c := defaultSECtx{
504 user: "bob",
505 level: "SystemLow-SystemHigh",
506 scon: "system_u:staff_r:staff_t:s0",
507 userRdr: bytes.NewBufferString(tt.userBuff),
508 defaultRdr: bytes.NewBufferString(tt.defaultBuff),
509 verifier: verifier,
510 }
511
512 got, err := getDefaultContextFromReaders(&c)
513 if err != nil {
514 t.Fatalf("err should not exist but is: %v", err)
515 }
516
517 if got != want {
518 t.Fatalf("got context: %q but expected %q", got, want)
519 }
520 })
521 }
522
523 t.Run("no match in user or default context files", func(t *testing.T) {
524 badUserBuff := ""
525
526 badDefaultBuff := `
527 foo_r:foo_t:s0 sysadm_r:sysadm_t:s0
528 dne_r:dne_t:s0 baz_r:baz_t:s0 sysadm_r:sysadm_t:s0
529 `
530 c := defaultSECtx{
531 user: "bob",
532 level: "SystemLow-SystemHigh",
533 scon: "system_u:staff_r:staff_t:s0",
534 userRdr: bytes.NewBufferString(badUserBuff),
535 defaultRdr: bytes.NewBufferString(badDefaultBuff),
536 verifier: verifier,
537 }
538
539 _, err := getDefaultContextFromReaders(&c)
540 if err == nil {
541 t.Fatalf("err was expected")
542 }
543 })
544 }
545
546 func BenchmarkChcon(b *testing.B) {
547 file, err := filepath.Abs(os.Args[0])
548 if err != nil {
549 b.Fatalf("filepath.Abs: %v", err)
550 }
551 dir := filepath.Dir(file)
552 con, err := FileLabel(file)
553 if err != nil {
554 b.Fatalf("FileLabel(%q): %v", file, err)
555 }
556 b.Logf("Chcon(%q, %q)", dir, con)
557 b.ResetTimer()
558 for n := 0; n < b.N; n++ {
559 if err := Chcon(dir, con, true); err != nil {
560 b.Fatal(err)
561 }
562 }
563 }
564
565 func BenchmarkCurrentLabel(b *testing.B) {
566 var (
567 l string
568 err error
569 )
570 for n := 0; n < b.N; n++ {
571 l, err = CurrentLabel()
572 if err != nil {
573 b.Fatal(err)
574 }
575 }
576 b.Log(l)
577 }
578
579 func BenchmarkReadConfig(b *testing.B) {
580 str := ""
581 for n := 0; n < b.N; n++ {
582 str = readConfig(selinuxTypeTag)
583 }
584 b.Log(str)
585 }
586
587 func BenchmarkLoadLabels(b *testing.B) {
588 for n := 0; n < b.N; n++ {
589 loadLabels()
590 }
591 }
592
View as plain text