From 16143217c55e70b592c8df72b3315c6bc3dbdfd6 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Sun, 1 Jan 2017 19:52:51 -0700 Subject: sd_login: Initial WIP implementation --- sd_login/.gitignore | 2 + sd_login/Makefile | 28 +++++++ sd_login/cgroup_generic.go | 163 ++++++++++++++++++++++++++++++++++++++++ sd_login/cgroup_skip.go | 98 ++++++++++++++++++++++++ sd_login/cgroup_skip_gen.go.gen | 41 ++++++++++ sd_login/cgroup_systemd.go | 107 ++++++++++++++++++++++++++ sd_login/doc.go | 38 ++++++++++ sd_login/gen.go | 3 + sd_login/missing.s | 0 sd_login/notes.org | 78 +++++++++++++++++++ sd_login/type_machine.go | 73 ++++++++++++++++++ sd_login/type_monitor.go | 106 ++++++++++++++++++++++++++ sd_login/type_pid.go.gen | 63 ++++++++++++++++ sd_login/type_seat.go | 37 +++++++++ sd_login/type_session.go | 67 +++++++++++++++++ sd_login/type_user.go | 57 ++++++++++++++ sd_login/util.go | 130 ++++++++++++++++++++++++++++++++ sd_login/util_syscall.go | 51 +++++++++++++ sd_login/util_valid.go | 141 ++++++++++++++++++++++++++++++++++ 19 files changed, 1283 insertions(+) create mode 100644 sd_login/.gitignore create mode 100644 sd_login/Makefile create mode 100644 sd_login/cgroup_generic.go create mode 100644 sd_login/cgroup_skip.go create mode 100755 sd_login/cgroup_skip_gen.go.gen create mode 100644 sd_login/cgroup_systemd.go create mode 100644 sd_login/doc.go create mode 100644 sd_login/gen.go create mode 100644 sd_login/missing.s create mode 100644 sd_login/notes.org create mode 100644 sd_login/type_machine.go create mode 100644 sd_login/type_monitor.go create mode 100755 sd_login/type_pid.go.gen create mode 100644 sd_login/type_seat.go create mode 100644 sd_login/type_session.go create mode 100644 sd_login/type_user.go create mode 100644 sd_login/util.go create mode 100644 sd_login/util_syscall.go create mode 100644 sd_login/util_valid.go diff --git a/sd_login/.gitignore b/sd_login/.gitignore new file mode 100644 index 0000000..84030d6 --- /dev/null +++ b/sd_login/.gitignore @@ -0,0 +1,2 @@ +/type_pid.go +/cgroup_skip_gen.go diff --git a/sd_login/Makefile b/sd_login/Makefile new file mode 100644 index 0000000..055ee05 --- /dev/null +++ b/sd_login/Makefile @@ -0,0 +1,28 @@ +# Copyright (C) 2016-2017 Luke Shumaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +files.src.gen += type_pid.go +files.src.gen += cgroup_skip_gen.go + +files.generate: $(files.src.gen) +maintainer-clean: + rm -f -- $(files.src.gen) +.PHONY: files.generate maintainer-clean + +%.go: %.go.gen + ./$^ > $@ + +cgroup_skip_gen.go: cgroup_skip.go + +.DELETE_ON_ERROR: diff --git a/sd_login/cgroup_generic.go b/sd_login/cgroup_generic.go new file mode 100644 index 0000000..3842c64 --- /dev/null +++ b/sd_login/cgroup_generic.go @@ -0,0 +1,163 @@ +// Copyright (C) 2016-2017 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "golang.org/x/sys/unix" +) + +type _Cgroup string + +var cgVersion_cache uint + +func cgVersion() uint { + if cgVersion_cache == 0 { + fs, err := statfs("/sys/fs/cgroup/") + if err != nil { + return 0 + } + if fs.Type == magic_CGROUP2_SUPER { + cgVersion_cache = 2 + } else if fs.Type == magic_TMPFS { + // XXX: systemd-specific cgroup v1 logic + fs, err = statfs("/sys/fs/cgroup/systemd/") + if err != nil { + return 0 + } + if fs.Type == magic_CGROUP2_SUPER { + cgVersion_cache = 2 + } else { + cgVersion_cache = 1 + } + } + } + return cgVersion_cache +} + +// getCgroup returns the cgroup path of the process, relative to the +// root of the cgroup hierarchy. +// +// In cgroup v2, there is a only one cgroup hierarchy, so the behavior +// is obvious. +// +// However, in cgroup v1, there were multiple cgroup hierarchies, so +// this function must decide which hierarchy to use. We chooses the +// first hierarchy with either the "name=systemd" controller or the +// "name=elogind" controller attached to it[1]. If no such hierarchy +// exists, then an error is returned. +// +// However, it is possible to generally use cgroup v1, but use a +// single (named) v2 hierarchy alongside many v1 hierarchies. In this +// case, we use the v2 hierarchy iff it is named "systemd", otherwise +// we use the cgroup v1 behavior. +// +// [1]: The "first" in that sentence is worrying; shouldn't the choice +// of hierarchy not depend on the undefined order that controllers are +// listed in? Well, a controller may be attached to only one +// hierarchy at a time. So there is only an ambiguity for "first" to +// come in if both "name=systemd" and "name=elogind" controllers +// exist. Systemd and elogind cannot be used together, so this isn't +// a concern. +// +// BUG(lukeshu): PID.getCgroup: Has systemd-specific logic. However, +// it is only for "legacy" cgroup v1 compatibility; the cgroup v2 +// logic is totally implementation-agnostic. Unfortunately(?), no +// distro seems to be using cgroup v2 (introduced in Linux 4.5) yet by +// default. +func (pid PID) getCgroup() (_Cgroup, error) { + cgVer := cgVersion() + + var cgroupFilename string + if pid == 0 { + cgroupFilename = "/proc/self/cgroup" + } else { + cgroupFilename = fmt.Sprintf("/proc/%d/cgroup", pid) + } + + f, err := os.Open(cgroupFilename) + if err != nil { + return "", err + } + defer f.Close() + + bf := bufio.NewReader(f) + + for { + line, err := bf.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return "", err + } + line = strings.TrimSuffix(line, "\n") + + parts := strings.SplitN(line, ":", 3) + if len(parts) != 3 { + continue + } + + hierarchy := parts[0] + controllers := parts[1] + path := _Cgroup(parts[2]) + + switch cgVer { + case 1: + for _, controller := range strings.Split(controllers, ",") { + if controller == "name=systemd" || controller == "name=elogind" { + return path, nil + } + } + case 2: + if hierarchy != "0" { + continue + } + return path, nil + } + } + return "", unix.ENODATA +} + +// cgGetRootPath determines the cgroup that all other cgroups belong +// to. The common case is just "/", but it could be something else if +// we are inside of a container, but have a view of the entier cgroup +// hierarchy. +// +// BUG(lukeshu): cgGetRootPath: works correctly on systemd and +// elogind, but I'm not sure it's general. +func cgGetRootPath() (_Cgroup, error) { + cgpath, err := PID(1).getCgroup() + if err != nil { + return "/", err + } + + cgpath = _Cgroup(trimOneSuffix(string(cgpath), + "/init.scope", // modern systemd + "/system.slice", // legacy systemd + "/system", // even more legacy systemd + )) + + return cgpath, nil +} + +func cgUnescape(s string) string { + return strings.TrimPrefix(s, "_") +} diff --git a/sd_login/cgroup_skip.go b/sd_login/cgroup_skip.go new file mode 100644 index 0000000..7eb2be8 --- /dev/null +++ b/sd_login/cgroup_skip.go @@ -0,0 +1,98 @@ +// Copyright (C) 2016-2017 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "strconv" + "strings" +) + +func (cgroup _Cgroup) SkipPath(prefix _Cgroup) (_Cgroup, bool) { + //skip: SkipPath(prefix _Cgroup) : SkipPath(prefix) + rest, ok := path_startswith(string(cgroup), string(prefix)) + if ok { + return _Cgroup(rest), true + } else { + return cgroup, false + } +} + +// Skip (*.slice){1,} +func (cgroup _Cgroup) SkipSlices() (_Cgroup, bool) { + //skip: SkipSlices() : SkipSlices() + cg := string(cgroup) + skipped := false + for { + cg = strings.TrimLeft(cg, "/") + part, rest := split2(cg, '/') + if !valid_slice_name(part) { + return _Cgroup(cg), skipped + } + skipped = true + cg = rest + } +} + +// Skip user@*.service +func (cgroup _Cgroup) SkipUserManager() (_Cgroup, bool) { + //skip: SkipUserManager() : SkipUserManager() + part, rest := split2(strings.TrimLeft(string(cgroup), "/"), '/') + uid_str, ok := trimPrefixSuffix(part, "user@", ".service") + if !ok { + return cgroup, false + } + _, err := strconv.Atoi(uid_str) + if err != nil { + return cgroup, false + } + return _Cgroup(rest), true +} + +// Skip session-*.scope +func (cgroup _Cgroup) SkipSession() (_Cgroup, bool) { + //skip: SkipSession() : SkipSession() + part, rest := split2(strings.TrimLeft(string(cgroup), "/"), '/') + session, ok := trimPrefixSuffix(part, "session-", ".scope") + if !ok { + return cgroup, false + } + if !valid_session_name(session) { + return cgroup, false + } + return _Cgroup(rest), true +} + +// Skip (/*.slice){0,}/(user@*.service|session-*.scope) +func (cgroup _Cgroup) SkipUserPrefix() (_Cgroup, bool) { + //skip: SkipUserPrefix() : SkipUserPrefix() + cgroup, _ = cgroup.SkipSlices() + cgroup, ok := cgroup.SkipUserManager() + if ok { + return cgroup, ok + } + return cgroup.SkipSession() +} + +// Skip cgGetRootPath +func (cgroup _Cgroup) SkipSystemPrefix() (_Cgroup, bool) { + //skip: SkipSystemPrefix() : SkipSystemPrefix() + + rootpath, err := cgGetRootPath() + if err != nil { + return cgroup, false + } + + return cgroup.SkipPath(rootpath) +} diff --git a/sd_login/cgroup_skip_gen.go.gen b/sd_login/cgroup_skip_gen.go.gen new file mode 100755 index 0000000..26c515a --- /dev/null +++ b/sd_login/cgroup_skip_gen.go.gen @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copyright (C) 2017 Luke Shumaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{ + printf '//' + printf ' %q' "$0" "$@" + printf '\n// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n\n' + + echo package sd_login + + grep -o '//skip:.*' "$1" | cut -d: -f2- | while read -r line; do + sig=$(echo $(cut -d: -f1 <<<"$line")) + cal=$(echo $(cut -d: -f2 <<<"$line")) + cat < +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "os" + "strconv" + "strings" +) + +// XXX: logind +func (cgroup _Cgroup) GetSession() Session { + unit := cgroup.GetUnit() + + session, ok := trimPrefixSuffix(unit, "session-", ".scope") + if !ok || !valid_session_name(session) { + return "" + } + + return Session(session) +} + +// XXX: who owns this, systemd or logind? elogind doesn't implement +// it, but idk +func (cgroup _Cgroup) GetOwnerUser() User { + slice := cgroup.GetSlice() + + uid_str, ok := trimPrefixSuffix(slice, "user-", ".slice") + if !ok { + return -1 + } + + uid, err := strconv.Atoi(uid_str) + if err != nil { + return -1 + } + + return User(uid) +} + +// XXX: machined +func (cgroup _Cgroup) GetMachine() Machine { + unit := cgroup.GetUnit() + if unit == "" { + return "" + } + + machine, err := os.Readlink("/run/systemd/machines/unit:" + unit) + if err != nil { + return "" + } + return Machine(machine) +} + +// XXX: systemd +func (cgroup _Cgroup) decodeUnit() string { + unit, _ := split2(string(cgroup), '/') + if len(unit) < 3 { + return "" + } + unit = cgUnescape(unit) + if valid_unit_name(unit)&(unit_name_plain|unit_name_instance) == 0 { + return "" + } + return unit +} + +func (cgroup _Cgroup) GetUnit() string { + unit := cgroup.MaybeSkipSlices().decodeUnit() + if strings.HasSuffix(unit, ".slice") { + return "" + } + return unit +} +func (cgroup _Cgroup) GetUserUnit() string { + return cgroup.MustSkipUserPrefix().GetUnit() +} +func (cgroup _Cgroup) GetSlice() string { + cg := string(cgroup) + n := 0 + for { + cg = strings.TrimLeft(cg, "/") + part, rest := split2(cg, '/') + if !valid_slice_name(part) { + if n == 0 { + return "-.slice" + } + return _Cgroup(cg).decodeUnit() + } + cg = rest + } +} +func (cgroup _Cgroup) GetUserSlice() string { + return cgroup.MustSkipUserPrefix().GetSlice() +} diff --git a/sd_login/doc.go b/sd_login/doc.go new file mode 100644 index 0000000..ba1a0f7 --- /dev/null +++ b/sd_login/doc.go @@ -0,0 +1,38 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package sd_login TODO +// +// The login manager manages 4 basic object types +// +// - machine: A container or virtual machine +// +// - seat: A set of hardware devices for a workspace; ie a screen +// and keyboard +// +// - session: A login session; tied to 0 or 1 seats (0 for things +// like SSH login) +// +// - user: Keeps track of logged in users; that is, users with 1 or +// more sessions +// +// TODO: why are machines part of this? +// +// - pid: A process may belong to a session, or directly to a user if +// it is not part of a session. This "belonging" is separate +// accounting by the login manager; it is NOT the same as the +// EUID/RUID. +// +// - peer: A peer is a process on the other end of a AF_UNIX socket. +package sd_login diff --git a/sd_login/gen.go b/sd_login/gen.go new file mode 100644 index 0000000..3e5b5ac --- /dev/null +++ b/sd_login/gen.go @@ -0,0 +1,3 @@ +//go:generate make + +package sd_login diff --git a/sd_login/missing.s b/sd_login/missing.s new file mode 100644 index 0000000..e69de29 diff --git a/sd_login/notes.org b/sd_login/notes.org new file mode 100644 index 0000000..0627142 --- /dev/null +++ b/sd_login/notes.org @@ -0,0 +1,78 @@ +/* Error codes: + * + * invalid input parameters → -EINVAL + * invalid fd → -EBADF + * process does not exist → -ESRCH + * cgroup does not exist → -ENOENT + * machine, session does not exist → -ENXIO + * requested metadata on object is missing → -ENODATA + */ + +* PID +** get_... +*** session +*** owner_uid +*** unit +*** user_unit +*** slice +*** user_slice +*** machine_name +*** cgroup +* peer +** get_... +*** session +*** owner_uid +*** unit +*** user_unit +*** slice +*** user_slice +*** machine_name +*** cgroup +* UID +** is_on_seat +** get_... +*** state +*** display +*** sessions +*** seats +* session +** is_active +** is_remote +** get_... +*** state +*** uid +*** seat +*** service +*** type +*** class +*** desktop +*** display +*** remote_host +*** remote_user +*** tty +*** vt +* seat +** can_multi_session +** can_tty +** can_graphical +** get_... +*** active +*** sessions +* machine +** get_... +*** class +*** ifindices +* top level +** get_... +*** seats +*** sessions +*** uids +*** machine_names +* login monitor +** new +** unref +** flush +** get_... +*** fd +*** events +*** timeout diff --git a/sd_login/type_machine.go b/sd_login/type_machine.go new file mode 100644 index 0000000..5892017 --- /dev/null +++ b/sd_login/type_machine.go @@ -0,0 +1,73 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +type Machine string + +// running VMs/containers +func GetMachines() ([]Machine, error) { + strs, err := get_files_in_directory("/run/systemd/machines/") + if err != nil { + return nil, err + } + var machines []Machine + for _, str := range strs { + if strings.HasPrefix(str, "unit:") || !valid_machine_name(str) { + continue + } + machines = append(machines, Machine(str)) + } + return machines, nil +} + +func (m Machine) GetClass() (string, error) { + env, err := parse_env_file("/run/systemd/machines/" + string(m)) + if err != nil { + return "", err + } + class, ok := env["CLASS"] + if !ok { + return "", unix.ENXIO // or EIO? + } + return class, nil +} + +func (m Machine) GetIfIndices() ([]int, error) { + env, err := parse_env_file("/run/systemd/machines/" + string(m)) + if err != nil { + return nil, err + } + netif, ok := env["NETIF"] + if !ok { + return nil, unix.ENXIO // or EIO? + } + + var ifindices []int + for _, word := range strings.FieldsFunc(netif, func(c rune) bool { return strings.ContainsRune(whitespace, c) }) { + ifindex, err := strconv.Atoi(word) + if err != nil { + return nil, err + } + ifindices = append(ifindices, ifindex) + } + return ifindices, nil +} diff --git a/sd_login/type_monitor.go b/sd_login/type_monitor.go new file mode 100644 index 0000000..50f601a --- /dev/null +++ b/sd_login/type_monitor.go @@ -0,0 +1,106 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "git.lukeshu.com/go/libgnulinux/inotify" + "golang.org/x/sys/unix" +) + +// Monitor monitors several types of events, so you don't have to do +// busy-polling. +// +// However, Monitor won't actually give you the event that happened, +// you'll need to read it yourself. +type Monitor struct { + in *inotify.Inotify +} + +type MonitorCategory uint + +const ( + MonitorSeats MonitorCategory = 1 << iota + MonitorSessions + MonitorUsers + MonitorMachines + + MonitorAll = MonitorSeats | MonitorSessions | MonitorUsers | MonitorMachines +) + +func NewMonitor(categories MonitorCategory) (*Monitor, error) { + if categories == 0 { + return nil, unix.EINVAL + } + + in, err := inotify.InotifyInit1(inotify.IN_CLOEXEC) + if err != nil { + return nil, err + } + + if categories&MonitorSeats != 0 { + _, err := in.AddWatch("/run/systemd/seats/", inotify.IN_MOVED_TO|inotify.IN_DELETE) + if err != nil { + in.Close() + return nil, err + } + } + + if categories&MonitorSessions != 0 { + _, err := in.AddWatch("/run/systemd/sessions/", inotify.IN_MOVED_TO|inotify.IN_DELETE) + if err != nil { + in.Close() + return nil, err + } + } + + if categories&MonitorUsers != 0 { + _, err := in.AddWatch("/run/systemd/users/", inotify.IN_MOVED_TO|inotify.IN_DELETE) + if err != nil { + in.Close() + return nil, err + } + } + + if categories&MonitorMachines != 0 { + _, err := in.AddWatch("/run/systemd/machines/", inotify.IN_MOVED_TO|inotify.IN_DELETE) + if err != nil { + in.Close() + return nil, err + } + } + + return &Monitor{in: in}, nil +} + +func (m *Monitor) Close() error { + return m.in.Close() +} + +func (m *Monitor) Flush() error { + for { + ev, err := m.in.ReadNonblock() + if err != nil { + return err + } + if ev.Wd < 0 { + return nil + } + } +} + +func (m *Monitor) Wait() error { + _, err := m.in.ReadBlock() + return err +} diff --git a/sd_login/type_pid.go.gen b/sd_login/type_pid.go.gen new file mode 100755 index 0000000..ec1bfe0 --- /dev/null +++ b/sd_login/type_pid.go.gen @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Copyright (C) 2016-2017 Luke Shumaker +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{ + printf '//' + printf ' %q' "$0" "$@" + printf '\n// MACHINE GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n\n' + + cat < +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +type Seat string + +// GetSeats returns a list of all currently avialable local seats. +func GetSeats() ([]Seat, error) { + strs, err := get_files_in_directory("/run/systemd/seats/") + if err != nil { + return nil, err + } + seats := make([]Seat, len(strs)) + for i := range strs { + seats[i] = Seat(strs[i]) + } + return seats, nil +} + +func (seat Seat) GetActive() (Session, User, error) +func (seat Seat) GetSessions() ([]Session, []User, error) + +func (seat Seat) CanMultiSession() bool +func (seat Seat) CanTTY() bool +func (seat Seat) CanGraphical() bool diff --git a/sd_login/type_session.go b/sd_login/type_session.go new file mode 100644 index 0000000..47856de --- /dev/null +++ b/sd_login/type_session.go @@ -0,0 +1,67 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +type Session string + +type SessionState int + +const ( + SessionOnline SessionState = iota + SessionActive + SessionClosing +) + +func GetSessions() ([]Session, error) { + strs, err := get_files_in_directory("/run/systemd/sessions/") + if err != nil { + return nil, err + } + sessions := make([]Session, len(strs)) + for i := range strs { + sessions[i] = Session(strs[i]) + } + return sessions, nil +} + +func (sess Session) IsActive() bool { + state, err := sess.GetState() + return err == nil && state == SessionActive +} +func (sess Session) IsRemote() bool +func (sess Session) GetState() (SessionState, error) +func (sess Session) GetUser() (User, error) +func (sess Session) GetSeat() (Seat, error) + +// PAM +func (sess Session) GetService() (string, error) + +// tty, x11, wayland, mir, unspecified +func (sess Session) GetType() (string, error) + +// user, greeter, lock-screen +func (sess Session) GetClass() (string, error) + +// GNOME, KDE, systemd-console +func (sess Session) GetDesktop() (string, error) + +// X11 DISPLAY +func (sess Session) GetDisplay() (string, error) +func (sess Session) GetRemoteHost() (string, error) + +// PAM +func (sess Session) GetRemoteUser() (string, error) +func (sess Session) GetTTY() (string, error) +func (sess Session) GetVT() (uint, error) diff --git a/sd_login/type_user.go b/sd_login/type_user.go new file mode 100644 index 0000000..81a5418 --- /dev/null +++ b/sd_login/type_user.go @@ -0,0 +1,57 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "strconv" +) + +type User int + +type UserState int + +const ( + UserOffline UserState = iota + UserLingering + UserOnline + UserActive + UserClosing +) + +// GetUsers returns a list of all users who currently have login +// sessions. +func GetUsers() ([]User, error) { + strs, err := get_files_in_directory("/run/systemd/users/") + if err != nil { + return nil, err + } + var users []User + for _, uid_str := range strs { + uid, err := strconv.Atoi(uid_str) + if err != nil { + continue + } + users = append(users, User(uid)) + } + return users, nil +} + +func (uid User) IsOnSeat(seat Seat, requireActive bool) bool +func (uid User) GetState() (UserState, error) + +// primary session +func (uid User) GetDisplay() (Session, error) +func (uid User) GetSessions(requireActive bool) ([]Session, error) +func (uid User) GetSeats(requireActive bool) ([]Seat, error) diff --git a/sd_login/util.go b/sd_login/util.go new file mode 100644 index 0000000..45df0e0 --- /dev/null +++ b/sd_login/util.go @@ -0,0 +1,130 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "io/ioutil" + "os" + "path" + "strings" +) + +const ( + whitespace = " \t\n\r" + newline = "\n\r" + digits = "0123456789" + lowercase_letters = "abcdefghijklmnopqrstuvwxyz" + uppercase_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + letters = lowercase_letters + uppercase_letters +) + +func trimOneSuffix(s string, suffixes ...string) string { + for _, suffix := range suffixes { + if strings.HasSuffix(s, suffix) { + return strings.TrimSuffix(s, suffix) + } + } + return s +} + +func trimPrefixSuffix(s, prefix, suffix string) (string, bool) { + if strings.HasPrefix(s, prefix) && strings.HasSuffix(s, suffix) { + s = strings.TrimPrefix(s, prefix) + s = strings.TrimSuffix(s, suffix) + return s, true + } + return "", false +} + +func split2(s string, b byte) (string, string) { + n := strings.IndexByte(s, b) + if n < 0 { + n = len(s) + } + return s[:n], s[n:] +} + +func path_startswith(apath string, aprefix string) (string, bool) { + if path.IsAbs(apath) != path.IsAbs(aprefix) { + return "", false + } + for { + apath = strings.TrimLeft(apath, "/") + aprefix = strings.TrimLeft(aprefix, "/") + + if aprefix == "" { + return apath, true + } + if apath == "" { + return "", false + } + + pathPart, pathRest := split2(apath, '/') + prefixPart, prefixRest := split2(aprefix, '/') + + if pathPart != prefixPart { + return "", false + } + + apath = pathRest + aprefix = prefixRest + } +} + +func get_files_in_directory(apath string) ([]string, error) { + files, err := ioutil.ReadDir(apath) + if err != nil { + return nil, err + } + var ret []string + for _, file := range files { + if file.Mode()&os.ModeType&^os.ModeSymlink == 0 { + ret = append(ret, file.Name()) + } + } + return ret, nil +} + +func parse_env_file(filename string) (map[string]string, error) + +/* + enum { + PRE_KEY, + KEY, + PRE_VALUE, + VALUE, + VALUE_ESCAPE, + SINGLE_QUOTE_VALUE, + SINGLE_QUOTE_VALUE_ESCAPE, + DOUBLE_QUOTE_VALUE, + DOUBLE_QUOTE_VALUE_ESCAPE, + COMMENT, + COMMENT_ESCAPE + } state = PRE_KEY; + +func parse_env_file(filename string) (map[string]string, error) { + contents, err := ioutil.ReadFile(filename) + + for contents != "" { + switch state { + case PRE_KEY: + if strings.ContainsRune("#;", contents[0]) { + sate = COMMENT + } else if strings.ContainsRune(whitespace, contents[0]) { + state = KEY + last_key_whitespace + } +} +*/ diff --git a/sd_login/util_syscall.go b/sd_login/util_syscall.go new file mode 100644 index 0000000..f3d12a8 --- /dev/null +++ b/sd_login/util_syscall.go @@ -0,0 +1,51 @@ +// Copyright (C) 2015-2017 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "net" + + "golang.org/x/sys/unix" +) + +const ( + // from + magic_TMPFS = 0x01021994 + magic_CGROUP_SUPER = 0x0027e0eb + magic_CGROUP2_SUPER = 0x63677270 // "cgrp" +) + +const ( + // POSIX says at least 255, but Linux isn't POSIX. + host_name_max = 64 +) + +// I borrowed this from libnslcd.git/nslcd_systemd (of which I am the +// author) -- lukeshu +func getpeercred(conn *net.UnixConn) (cred unix.Ucred, err error) { + file, err := conn.File() + if err != nil { + return + } + defer file.Close() + _cred, err := unix.GetsockoptUcred(int(file.Fd()), unix.SOL_SOCKET, unix.SO_PEERCRED) + cred = *_cred + return +} + +func statfs(fspath string) (fs unix.Statfs_t, err error) { + err = unix.Statfs(fspath, &fs) + return +} diff --git a/sd_login/util_valid.go b/sd_login/util_valid.go new file mode 100644 index 0000000..e0dd906 --- /dev/null +++ b/sd_login/util_valid.go @@ -0,0 +1,141 @@ +// Copyright (C) 2016 Luke Shumaker +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sd_login + +import ( + "strings" +) + +func valid_session_name(id string) bool { + if id == "" { + return false + } + + return strings.TrimLeft(id, letters+digits) == "" +} + +func valid_slice_name(s string) bool { + return strings.HasSuffix(s, ".slice") && valid_unit_name(s) == unit_name_plain +} + +func valid_machine_name(s string) bool { + if len(s) > host_name_max { + // Note that Linux HOST_NAME_MAX is 64, but DNS allows + // 255, so names from DNS might be invalid. + return false + } + + n_dots := 0 + dot := true + for _, c := range s { + if c == '.' { + if dot { + return false + } + dot = true + n_dots++ + } else { + if !strings.ContainsRune(letters+digits+"-_.", c) { + return false + } + dot = false + } + } + + if dot { // trailing dot or empty + return false + } + + return true +} + +const ( + unit_name_invalid = 0 + unit_name_plain = 1 << 0 // foo.service + unit_name_instance = 1 << 1 // foo@bar.service + unit_name_template = 1 << 2 // foo@.service + unit_name_any = unit_name_plain | unit_name_instance | unit_name_template +) + +func valid_unit_name(unit string) int { + const_unit_name_max := 256 + const_unit_types := []string{ + "service", + "socket", + "busname", + "target", + "device", + "mount", + "automount", + "swap", + "timer", + "path", + "slice", + "scope", + } + + if unit == "" { + return unit_name_invalid + } + + // If the unit name is too long + if len(unit) >= const_unit_name_max { + return unit_name_invalid + } + + // If there is no dot in the unit name + dot := strings.LastIndexByte(unit, '.') + if dot < 0 || dot == 0 { + return unit_name_invalid + } + + // If the .suffix isn't a real unit type + utype := unit[dot+1:] + found := false + for _, _utype := range const_unit_types { + if utype == _utype { + found = true + break + } + } + if !found { + return unit_name_invalid + } + + at := strings.IndexByte(unit, '@') + + // If the unit has more than one '@' + if at >= 0 && strings.IndexByte(unit[at+1:], '@') >= 0 { + return unit_name_invalid + } + + // If the '@' is at the start of the unit + if at == 0 { + return unit_name_invalid + } + + if at < 0 { + return unit_name_plain + } else { + if dot > at+1 { + return unit_name_instance + } + if dot == at+1 { + return unit_name_template + } + } + + return unit_name_invalid +} -- cgit v1.1-4-g5e80