From 24b7a28336b903b7a8fac3588e48e821081929ac Mon Sep 17 00:00:00 2001
From: RandomChars <random@chars.jp>
Date: Thu, 2 Sep 2021 12:51:36 +0900
Subject: [PATCH] private mode

---
 api.go           | 91 ++++++++++++++++++++++++++++++++++++++++++++++++
 api/paths.go     |  1 +
 client/remote.go | 19 +++++++---
 config.go        | 38 +++++++++++++++-----
 store/store.go   |  5 ++-
 5 files changed, 140 insertions(+), 14 deletions(-)

diff --git a/api.go b/api.go
index 5d78674..74ffd6d 100644
--- a/api.go
+++ b/api.go
@@ -28,11 +28,23 @@ func registerAPI() {
 		context.JSON(http.StatusOK, instance.SingleUser)
 	})
 
+	router.GET(api.Private, func(context *gin.Context) {
+		context.JSON(http.StatusOK, instance.Private)
+	})
+
 	router.GET(api.User, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.Users())
 	})
 
 	router.PUT(api.User, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		user, ok := getUser(context)
 		if !instance.Register {
 			if !ok {
@@ -73,6 +85,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.UserField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		info := instance.User(context.Param("flake"))
 		context.JSON(http.StatusOK, api.UserPayload{
 			Username:   info.Username,
@@ -176,6 +192,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.UsernameField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		info := instance.UserUsername(context.Param("name"))
 		payload := api.UserPayload{
 			Username:   info.Username,
@@ -212,6 +232,7 @@ func registerAPI() {
 			return
 		}
 
+		// Only allow lookup if user is current user or privileged
 		flake := context.Param("flake")
 		if !info.Privileged && (info.Snowflake != flake) {
 			context.JSON(http.StatusForbidden, api.Denied)
@@ -229,6 +250,7 @@ func registerAPI() {
 			return
 		}
 
+		// Only allow set if user is current user or privileged
 		flake := context.Param("flake")
 		if !info.Privileged && (info.Snowflake != flake) {
 			context.JSON(http.StatusForbidden, api.Denied)
@@ -238,10 +260,18 @@ func registerAPI() {
 	})
 
 	router.GET(api.UserImage, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.UserImages(context.Param("flake")))
 	})
 
 	router.GET(api.SearchField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		tagsPayload := context.Param("tags")
 		tags := strings.Split(tagsPayload, "!")
 		context.JSON(http.StatusOK, instance.ImageSearch(tags))
@@ -294,10 +324,18 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImagePage, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.String(http.StatusOK, strconv.Itoa(instance.PageTotal(store.ImageRootPageVariant)))
 	})
 
 	router.GET(api.ImagePageField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		param := context.Param("entry")
 		entry, err := strconv.Atoi(param)
 		if err != nil {
@@ -308,6 +346,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImagePageImage, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		param := context.Param("entry")
 		entry, err := strconv.Atoi(param)
 		if err != nil {
@@ -318,6 +360,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImageField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.ImageSnowflake(context.Param("flake")))
 	})
 
@@ -364,6 +410,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImageFile, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		flake := context.Param("flake")
 		image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), false)
 		if image.Snowflake != flake {
@@ -374,6 +424,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImagePreview, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		flake := context.Param("flake")
 		image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), true)
 		if image.Snowflake != flake {
@@ -384,6 +438,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.ImageTag, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.ImageTags(context.Param("flake")))
 	})
 
@@ -422,6 +480,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.Tag, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.Tags())
 	})
 
@@ -474,6 +536,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.TagPage, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		tag := context.Param("tag")
 		if !instance.MatchName(tag) {
 			context.JSON(http.StatusBadRequest, api.Denied)
@@ -483,6 +549,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.TagPageField, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		tag := context.Param("tag")
 		if !instance.MatchName(tag) {
 			context.JSON(http.StatusBadRequest, api.Denied)
@@ -499,6 +569,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.TagPageImage, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		tag := context.Param("tag")
 		if !instance.MatchName(tag) {
 			context.JSON(http.StatusBadRequest, api.Denied)
@@ -515,6 +589,10 @@ func registerAPI() {
 	})
 
 	router.GET(api.TagInfo, func(context *gin.Context) {
+		if !privateAccessible(context) {
+			return
+		}
+
 		context.JSON(http.StatusOK, instance.TagInfo(context.Param("tag")))
 	})
 
@@ -541,6 +619,19 @@ func registerAPI() {
 	})
 }
 
+func privateAccessible(context *gin.Context) bool {
+	if !instance.Private {
+		return true
+	}
+
+	if _, ok := getUser(context); !ok {
+		context.JSON(http.StatusForbidden, api.Denied)
+		return false
+	}
+
+	return true
+}
+
 func getUser(context *gin.Context) (store.User, bool) {
 	if instance.SingleUser {
 		return instance.User(instance.InitialUser), true
diff --git a/api/paths.go b/api/paths.go
index 13fd584..4caec4a 100644
--- a/api/paths.go
+++ b/api/paths.go
@@ -3,6 +3,7 @@ package api
 const (
 	Base           = "/api"
 	SingleUser     = Base + "/single_user"
+	Private        = Base + "/private"
 	Image          = Base + "/image"
 	ImagePage      = Image + "/page"
 	ImagePageField = ImagePage + "/:entry"
diff --git a/client/remote.go b/client/remote.go
index 4f14410..4f3248a 100644
--- a/client/remote.go
+++ b/client/remote.go
@@ -9,11 +9,12 @@ import (
 
 // Remote represents a remote image board server.
 type Remote struct {
-	url    string
-	single bool
-	secret string
-	client *http.Client
-	user   *api.UserPayload
+	url     string
+	single  bool
+	private bool
+	secret  string
+	client  *http.Client
+	user    *api.UserPayload
 	store.Info
 }
 
@@ -33,11 +34,19 @@ func (r *Remote) SingleUser() bool {
 	return r.single
 }
 
+// Private returns whether the Remote is running in private mode.
+func (r *Remote) Private() bool {
+	return r.private
+}
+
 // Handshake checks if the server is still online and updates Remote.
 func (r *Remote) Handshake() error {
 	if err := r.fetch(http.MethodGet, api.SingleUser, &r.single, nil); err != nil {
 		return err
 	}
+	if err := r.fetch(http.MethodGet, api.Private, &r.private, nil); err != nil {
+		return err
+	}
 	return r.fetch(http.MethodGet, api.Base, r, nil)
 }
 
diff --git a/config.go b/config.go
index cbe36a1..58867fa 100644
--- a/config.go
+++ b/config.go
@@ -5,6 +5,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/viper"
 	"random.chars.jp/git/image-board/store"
+	"strconv"
 )
 
 var instance *store.Store
@@ -23,6 +24,7 @@ func configSetup() {
 		"loglevel":    "info",
 		"store":       "./db",
 		"single-user": true,
+		"private":     false,
 	})
 	viper.SetDefault("server", map[string]interface{}{
 		"host":  "127.0.0.1",
@@ -58,7 +60,8 @@ func configSetup() {
 		if viper.GetStringMap("server")["unix"] != serverConfig["unix"] ||
 			viper.GetStringMap("server")["host"] != serverConfig["host"] ||
 			viper.GetStringMap("server")["port"] != serverConfig["port"] ||
-			viper.GetStringMap("system")["single-user"] != systemConfig["single-user"] {
+			viper.GetStringMap("system")["single-user"] != systemConfig["single-user"] ||
+			viper.GetStringMap("system")["private"] != systemConfig["private"] {
 			log.Warn("Configuration change requires restart.")
 			cleanup(true)
 			return
@@ -76,12 +79,11 @@ func configSetup() {
 }
 
 func openStore() {
-	path := viper.GetStringMap("system")["store"].(string)
-	single, ok := viper.GetStringMap("system")["single-user"].(bool)
-	if !ok {
-		single = false
-	}
-	instance = store.New(path, single)
+	path := systemConfig["store"].(string)
+	single := parseBool(systemConfig["single-user"])
+	private := parseBool(systemConfig["private"])
+
+	instance = store.New(path, single, private)
 	if instance == nil {
 		log.Fatalf("Error initializing store.")
 	}
@@ -98,6 +100,26 @@ func openStore() {
 		}
 	}
 	if single {
-		log.Infof("Server running in single user mode, all operations are performed as the initial user.")
+		log.Info("Server running in single user mode, all operations are performed as the initial user.")
+	} else if private {
+		log.Info("Server running in private mode, all operations will require authentication.")
+	}
+}
+
+func parseBool(v interface{}) bool {
+	if s, ok := v.(bool); !ok {
+		var sS string
+		if sS, ok = v.(string); !ok {
+			return false
+		} else {
+			if b, err := strconv.ParseBool(sS); err != nil {
+				log.Warnf("Error parsing boolean value, %s", err)
+				return false
+			} else {
+				return b
+			}
+		}
+	} else {
+		return s
 	}
 }
diff --git a/store/store.go b/store/store.go
index 6a5f8e1..b96489b 100644
--- a/store/store.go
+++ b/store/store.go
@@ -33,6 +33,7 @@ type Info struct {
 type Store struct {
 	Path           string
 	SingleUser     bool
+	Private        bool
 	Revision       int
 	Compat         bool
 	Register       bool
@@ -58,7 +59,7 @@ func init() {
 }
 
 // New initialises a new store instance.
-func New(path string, single bool) *Store {
+func New(path string, single, private bool) *Store {
 	var store *Store
 	if stat, err := os.Stat(path); err != nil {
 		log.Infof("Initializing new store at %s.", path)
@@ -66,6 +67,7 @@ func New(path string, single bool) *Store {
 		store = &Store{
 			Path:           path,
 			SingleUser:     single,
+			Private:        private,
 			Revision:       revision,
 			Compat:         runtime.GOOS == "windows",
 			Register:       false,
@@ -101,6 +103,7 @@ func New(path string, single bool) *Store {
 		store = &Store{
 			Path:           path,
 			SingleUser:     single,
+			Private:        private,
 			Revision:       info.Revision,
 			Compat:         info.Compat,
 			Register:       info.Register,
-- 
GitLab