|
|
|
@ -5,6 +5,7 @@
|
|
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
_ "embed"
|
|
|
|
@ -15,11 +16,13 @@ import (
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/cgi"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/peterbourgon/ff/v2/ffcli"
|
|
|
|
|
"go4.org/mem"
|
|
|
|
|
"tailscale.com/client/tailscale"
|
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
|
"tailscale.com/tailcfg"
|
|
|
|
@ -82,17 +85,63 @@ func runWeb(ctx context.Context, args []string) error {
|
|
|
|
|
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func auth() (string, error) {
|
|
|
|
|
// authorize checks whether the provided user has access to the web UI.
|
|
|
|
|
func authorize(name string) error {
|
|
|
|
|
if distro.Get() == distro.Synology {
|
|
|
|
|
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("auth: %v: %s", err, out)
|
|
|
|
|
return authorizeSynology(name)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// authorizeSynology checks whether the provided user has access to the web UI
|
|
|
|
|
// by consulting the membership of the "administrators" group.
|
|
|
|
|
func authorizeSynology(name string) error {
|
|
|
|
|
f, err := os.Open("/etc/group")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer f.Close()
|
|
|
|
|
s := bufio.NewScanner(f)
|
|
|
|
|
var agLine string
|
|
|
|
|
for s.Scan() {
|
|
|
|
|
if !mem.HasPrefix(mem.B(s.Bytes()), mem.S("administrators:")) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return string(out), nil
|
|
|
|
|
agLine = s.Text()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if err := s.Err(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if agLine == "" {
|
|
|
|
|
return fmt.Errorf("admin group not defined")
|
|
|
|
|
}
|
|
|
|
|
agEntry := strings.Split(agLine, ":")
|
|
|
|
|
if len(agEntry) < 4 {
|
|
|
|
|
return fmt.Errorf("malformed admin group entry")
|
|
|
|
|
}
|
|
|
|
|
agMembers := agEntry[3]
|
|
|
|
|
for _, m := range strings.Split(agMembers, ",") {
|
|
|
|
|
if m == name {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("not a member of administrators group")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", nil
|
|
|
|
|
// authenticate returns the name of the user accessing the web UI.
|
|
|
|
|
// Note: This is different from a tailscale user, and is typically the local
|
|
|
|
|
// user on the node.
|
|
|
|
|
func authenticate() (string, error) {
|
|
|
|
|
if distro.Get() != distro.Synology {
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("auth: %v: %s", err, out)
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(string(out)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
@ -198,8 +247,13 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user, err := auth()
|
|
|
|
|
user, err := authenticate()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := authorize(user); err != nil {
|
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|