|
|
@@ -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
|
|
|
}
|