feat: web support http basic authorization (#170)

This commit is contained in:
Levi authored and GitHub committed 2023-07-27 21:02:33 +08:00
1 parent 7c2b81caf7
commit 81518d794f
5 files changed
+179 -18

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
)
type args struct {
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
configPath *string
}
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
fmt.Println("parse")
// args priority: config file > cli args
cfgArgs := loadConfig(*cliArgs.configPath)
if cfgArgs.Address == nil {
cfgArgs.Address = cliArgs.Address
}
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
return cfgArgs
}
func loadConfig(path string) *args {
var args args
if !filepath.IsAbs(path) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return &args
}
path = filepath.Join(dir, path)
}
file, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &args
}
fmt.Println("config file read failed, reason:" + err.Error())
return &args
}
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error())
return &args
}
fmt.Printf("config file loaded: %s\n", path)
return &args
}
+22 -5
View File
@@ -5,6 +5,7 @@ package main
import (
"embed"
"fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
@@ -18,12 +19,28 @@ func main() {
if err != nil {
panic(err)
}
args := parse()
var webBasicAuth *model.WebBasicAuth
if isNotBlank(args.Username) && isNotBlank(args.Password) {
webBasicAuth = &model.WebBasicAuth{
Username: *args.Username,
Password: *args.Password,
}
}
cfg := &model.StartConfig{
Network: "tcp",
Address: "0.0.0.0:9999",
Storage: model.StorageBolt,
WebEnable: true,
WebFS: sub,
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
ApiToken: *args.ApiToken,
WebEnable: true,
WebFS: sub,
WebBasicAuth: webBasicAuth,
}
cmd.Start(cfg)
}
func isNotBlank(str *string) bool {
return str != nil && *str != ""
}
+18 -3
View File
@@ -1,6 +1,9 @@
package model
import "io/fs"
import (
"encoding/base64"
"io/fs"
)
type Storage string
@@ -17,8 +20,9 @@ type StartConfig struct {
StorageDir string `json:"storageDir"`
ApiToken string `json:"apiToken"`
WebEnable bool
WebFS fs.FS
WebEnable bool
WebFS fs.FS
WebBasicAuth *WebBasicAuth
}
func (cfg *StartConfig) Init() *StartConfig {
@@ -39,3 +43,14 @@ func (cfg *StartConfig) Init() *StartConfig {
}
return cfg
}
type WebBasicAuth struct {
Username string
Password string
}
// Authorization returns the value of the Authorization header to be used in HTTP requests.
func (cfg *WebBasicAuth) Authorization() string {
userId := cfg.Username + ":" + cfg.Password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
}
+16 -5
View File
@@ -98,14 +98,21 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
if startCfg.ApiToken != "" {
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Api-Token") != startCfg.ApiToken {
WriteJson(w, model.NewErrorResult("invalid token", model.CodeUnauthorized))
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
h.ServeHTTP(w, r)
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
})
}
@@ -127,7 +134,11 @@ func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
}
func WriteJson(w http.ResponseWriter, v any) {
WriteStatusJson(w, http.StatusOK, v)
}
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(v)
}
+46 -5
View File
@@ -273,7 +273,7 @@ func TestDoProxy(t *testing.T) {
})
}
func TestAuthorization(t *testing.T) {
func TestApiToken(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
@@ -285,16 +285,57 @@ func TestAuthorization(t *testing.T) {
Stop()
}()
code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil)
checkCode(code, model.CodeUnauthorized)
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusUnauthorized)
}
code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
checkCode(code, model.CodeOk)
if status != http.StatusOK {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
}
}
func TestAuthorization(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
cfg.WebEnable = true
cfg.WebBasicAuth = &model.WebBasicAuth{
Username: "admin",
Password: "123456",
}
fileListener := doStart(cfg)
defer func() {
if err := fileListener.Close(); err != nil {
panic(err)
}
Stop()
}()
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
}
func doTest(handler func()) {
testFunc := func(storage model.Storage) {
var cfg = &model.StartConfig{}