SSHSecure/utils/files.go

173 lines
4.0 KiB
Go

/*
SSHSecure - a program to harden OpenSSH from defaults
Copyright (C) 2020 Brent Saner
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package utils
import (
"encoding/gob"
"errors"
"fmt"
"os"
gopath "path"
"path/filepath"
"time"
"r00t2.io/sysutils/paths"
"r00t2.io/sshsecure/sharedconsts"
)
func backupWrite(path string, data []byte, system bool, force bool) error {
var err error
var uid int
var gid int
var running bool
var runningInfo runInfo
var curInfo runInfo
uid, gid, err = getPerms(system)
if err != nil {
return err
}
if err = paths.RealPath(&path); err != nil {
return err
}
curInfo = getInfo()
parent := filepath.Dir(path)
parentExists, err := paths.RealPathExists(&parent)
infoFile := gopath.Join(
parent,
fmt.Sprintf(".%v.gob",
filepath.Base(path),
))
if err = paths.MakeDirIfNotExist(&parent); err != nil {
return err
}
exists, err := paths.RealPathExists(&path)
if err != nil {
return err
} else if !parentExists {
// It exists *now*, but it's a new dir. We set perms thusly.
os.Chmod(parent, 0700)
}
if gobExists, err := paths.RealPathExists(&infoFile); err != nil {
return err
} else if gobExists && !force {
return nil // Don't go any further; the file was already generated.
}
running, runningInfo, err = isRunning()
if err != nil {
return err
}
if exists && running {
e := fmt.Sprintf(
"refusing to write to %v; another instance of program is currently running with PID %v started at %v",
path, runningInfo.Pid, runningInfo.TimeStarted)
return errors.New(e)
} else if exists && !running {
newName := fmt.Sprintf("%v.bak.%d", path, curInfo.TimeStarted.Unix())
if err = os.Rename(path, newName); err != nil {
return err
}
}
// FINALLY. The business end.
w, err := os.Create(path)
if err != nil {
return err
}
gobFile, err := os.Create(infoFile)
if err != nil {
return err
}
g := gob.NewEncoder(gobFile)
defer w.Close()
defer gobFile.Close()
if err = w.Chmod(0600); err != nil {
return err
}
if err = w.Chown(uid, gid); err != nil {
return err
}
if _, err = w.Write(data); err != nil {
return err
}
if err = gobFile.Chmod(0600); err != nil {
return err
}
if err = gobFile.Chown(uid, gid); err != nil {
return err
}
if err = g.Encode(curInfo); err != nil {
return err
}
return nil
}
func getInfo() runInfo {
curInfo := runInfo{
TimeStarted: getTime(),
Pid: getPid(),
}
return curInfo
}
func getPid() int {
return os.Getpid()
}
func getPerms(system bool) (int, int, error) {
var uid int
var gid int
if system {
if os.Geteuid() != 0 {
return 0, 0, errors.New("cannot create system file without root permissions")
}
uid = 0
gid = 0
} else {
uid = os.Geteuid()
gid = os.Getegid()
}
return uid, gid, nil
}
func getTime() time.Time {
return time.Now()
}
func isRunning() (bool, runInfo, error) {
curInfo := getInfo()
var fileInfo runInfo
lckFile := sharedconsts.LockFile
exists, _ := paths.RealPathExists(&lckFile)
if !exists {
// This should never happen. If it does, I goofed the locking (or it's not invoked from a shell).
return false, fileInfo, errors.New("*grabs shotgun* computer's haunted")
}
reader, err := os.Open(lckFile)
if err != nil {
return false, fileInfo, err
}
defer reader.Close()
gobDecoder := gob.NewDecoder(reader)
gobDecoder.Decode(&fileInfo)
if fileInfo != curInfo {
return true, fileInfo, nil
}
return false, fileInfo, nil
}