refact: use klog and ksrv
continuous-integration/drone/tag Build is passing Details

feat/muzan v0.1.7
Evan Chen 2021-11-04 03:35:51 +08:00
parent cff4c13b78
commit 239be22094
10 changed files with 136 additions and 206 deletions

View File

@ -3,11 +3,11 @@ package main
import (
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
log "kumoly.io/lib/klog"
"kumoly.io/tools/configui"
)
@ -55,17 +55,6 @@ func main() {
cui := configui.New()
// setup logging
if flagLogFile != "" {
f, err := os.OpenFile(flagLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Error opening file: %v", err)
}
defer f.Close()
mwriter := io.MultiWriter(f, os.Stderr)
log.SetOutput(mwriter)
}
// setup values
if flagPath != "" {
if flagName == "" {
@ -77,18 +66,21 @@ func main() {
Action: flagAction,
}
if err := cui.AppendFile(file); err != nil {
log.Fatalln(err)
log.Error(err)
os.Exit(1)
}
} else if flagConfigPath == "" {
log.Println("no config specified")
log.Error("no config specified")
} else {
conf, err := os.ReadFile(flagConfigPath)
if err != nil {
log.Fatalln(err)
log.Error(err)
os.Exit(1)
}
cui.LoadConfig(string(conf))
if err != nil {
log.Fatalln(err)
log.Error(err)
os.Exit(1)
}
}
@ -114,6 +106,10 @@ func main() {
}
// start server
log.Println("Listening on", flagBind)
log.Fatal(server.ListenAndServe())
log.Info("Listening on ", flagBind)
err := server.ListenAndServe()
if err != nil {
log.Error(err)
os.Exit(1)
}
}

View File

@ -1,13 +1,17 @@
package configui
import (
"bytes"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"net/http"
"os"
"time"
"kumoly.io/lib/klog"
"kumoly.io/tools/configui/public"
)
@ -30,6 +34,7 @@ var Ext2Mode map[string]string = map[string]string{
type ConfigUI struct {
AppName string `json:"app_name"`
Prod bool `json:"production"`
BaseUrl string `json:"base_url"`
ConfigPath string `json:"config_path"`
@ -45,12 +50,15 @@ type ConfigUI struct {
HideConfig bool `json:"hide_config"`
// Should be in main app
LogPath string `json:"log_path"`
SilentSysOut bool `json:"silent_sys_out"`
LogPath string `json:"log_path"`
LogLevel int `json:"log_level"`
TmplFS embed.FS `json:"-"`
tmpl *template.Template
PublicFS embed.FS `json:"-"`
PublicFS embed.FS `json:"-"`
log *klog.Logger `json:"-"`
ksrv_log *klog.Logger `json:"-"`
f *os.File `json:"-"`
}
func New() *ConfigUI {
@ -65,6 +73,7 @@ func New() *ConfigUI {
}).ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
return &ConfigUI{
fileIndex: map[string]int{},
Prod: true,
AppName: "ConfigUI",
BaseUrl: "/",
PublicFS: public.FS,
@ -72,15 +81,18 @@ func New() *ConfigUI {
tmpl: tmpl,
CmdTimeout: "10s",
cmdTimeout: time.Second * 10,
LogLevel: klog.Lerror | klog.Linfo,
log: klog.Sub("ConfigUI"),
}
}
func (cui *ConfigUI) File(file_name string) (*File, error) {
if file_name == cui.AppName {
return &File{
Path: cui.ConfigPath,
Name: cui.AppName,
Lang: "json",
Path: cui.ConfigPath,
Name: cui.AppName,
Lang: "json",
owner: cui,
}, nil
}
index, ok := cui.fileIndex[file_name]
@ -103,6 +115,7 @@ func (cui *ConfigUI) LoadConfig(confstr string) error {
if f.Name == "" {
f.Name = f.Path
}
f.owner = cui
tmpIndex[f.Name] = i
}
@ -110,15 +123,21 @@ func (cui *ConfigUI) LoadConfig(confstr string) error {
cui.fileIndex = tmpIndex
cui.Files = tmpConf.Files
cui.AllowIP = tmpConf.AllowIP
cui.Prod = tmpConf.Prod
cui.ConfigPath = tmpConf.ConfigPath
cui.HideConfig = tmpConf.HideConfig
cui.LogPath = tmpConf.LogPath
cui.NoReconfig = tmpConf.NoReconfig
cui.AppName = tmpConf.AppName
if cui.AppName == "" {
cui.AppName = "ConfigUI"
}
cui.BaseUrl = tmpConf.BaseUrl
if cui.BaseUrl == "" {
cui.BaseUrl = "/"
}
ct, err := time.ParseDuration(tmpConf.CmdTimeout)
if err != nil || cui.CmdTimeout == "" {
cui.CmdTimeout = "10s"
@ -128,13 +147,48 @@ func (cui *ConfigUI) LoadConfig(confstr string) error {
cui.cmdTimeout = ct
}
cui.log = klog.Sub(cui.AppName)
// NOT implemented
cui.ResultBellow = tmpConf.ResultBellow
cui.SilentSysOut = tmpConf.SilentSysOut
cui.LogLevel = tmpConf.LogLevel
cui.LogPath = tmpConf.LogPath
cui.setLog()
// fmt.Printf("%+v", cui)
return nil
}
func (cui *ConfigUI) setLog() {
var err error
if cui.f != nil {
cui.f.Close()
}
if cui.LogPath != "" {
cui.f, err = os.OpenFile(cui.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
cui.log.Error(err)
}
cui.log.SetErrOutput(cui.f)
cui.log.SetOutput(cui.f)
cui.log.Reload()
if cui.ksrv_log != nil {
cui.ksrv_log.SetErrOutput(cui.f)
cui.ksrv_log.SetOutput(cui.f)
cui.ksrv_log.Reload()
}
} else {
cui.log.SetErrOutput(os.Stderr)
cui.log.SetOutput(os.Stderr)
cui.log.Reload()
if cui.ksrv_log != nil {
cui.ksrv_log.SetErrOutput(os.Stderr)
cui.ksrv_log.SetOutput(os.Stderr)
cui.ksrv_log.Reload()
}
}
}
func (cui *ConfigUI) Config() ([]byte, error) {
return json.MarshalIndent(cui, "", " ")
}
@ -143,6 +197,7 @@ func (cui *ConfigUI) AppendFile(file *File) error {
if file.Name == "" {
file.Name = file.Path
}
file.owner = cui
i, ok := cui.fileIndex[file.Name]
if ok {
return fmt.Errorf("%v already exists at %d", file.Name, i)
@ -151,3 +206,13 @@ func (cui *ConfigUI) AppendFile(file *File) error {
cui.Files = append(cui.Files, file)
return nil
}
func (cui *ConfigUI) Parse(w http.ResponseWriter, name string, data interface{}) error {
buf := &bytes.Buffer{}
err := cui.tmpl.ExecuteTemplate(buf, "home", data)
if err != nil {
panic(err)
}
_, err = w.Write(buf.Bytes())
return err
}

35
file.go
View File

@ -1,9 +1,7 @@
package configui
import (
"encoding/json"
"errors"
"log"
"os"
"os/exec"
"runtime"
@ -25,7 +23,8 @@ type File struct {
// used for parsing post data
Data string `json:"data"`
lock sync.RWMutex `json:"-"`
lock sync.RWMutex `json:"-"`
owner *ConfigUI `json:"-"`
}
func (f *File) Read() ([]byte, error) {
@ -33,7 +32,7 @@ func (f *File) Read() ([]byte, error) {
defer f.lock.RUnlock()
data, err := os.ReadFile(f.Path)
if err != nil {
log.Println(err)
f.owner.log.Error(err)
return nil, err
}
return data, nil
@ -64,7 +63,7 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) {
} else {
cmd = exec.Command(UNIX_SHELL, "-c", f.Action)
}
log.Println("DO: ", f.Action)
f.owner.log.Info("DO: ", f.Action)
done := make(chan string, 1)
go func() {
out, _ := cmd.CombinedOutput()
@ -77,32 +76,10 @@ func (f *File) Do(CmdTimeout time.Duration) (string, error) {
select {
case <-time.After(CmdTimeout):
cmd.Process.Kill()
log.Println("timeout")
f.owner.log.Error("timeout")
return "", errors.New("command timeout")
case out := <-done:
log.Printf("\n%v", out)
f.owner.log.Info("\n", out)
return out, nil
}
}
func ReadConfig(confstr string) ([]File, error) {
conf := []File{}
err := json.Unmarshal([]byte(confstr), &conf)
if err != nil {
return nil, err
}
for i := range conf {
if conf[i].Name == "" {
conf[i].Name = conf[i].Path
}
}
return conf, nil
}
func GetFileMap(files []File) map[string]*File {
fileMap := map[string]*File{}
for i := range files {
fileMap[files[i].Name] = &files[i]
}
return fileMap
}

10
go.mod
View File

@ -1,3 +1,13 @@
module kumoly.io/tools/configui
go 1.17
require (
kumoly.io/lib/klog v0.1.9
kumoly.io/lib/ksrv v0.1.4
)
require (
github.com/mattn/go-isatty v0.0.14 // indirect
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
)

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
kumoly.io/lib/klog v0.1.9 h1:rS9PPqfyBIIfeQlPSuMv+7StGPiFVuAdp04HDwwDY3E=
kumoly.io/lib/klog v0.1.9/go.mod h1:Snm+c1xRrh/RbXsxQf7UGYbAJGPcIa6bEEN+CmzJh7M=
kumoly.io/lib/ksrv v0.1.4 h1:8zbslRwdNWHw5Wm1PiyDLr6Mu4xxjz0FTW4u8dZ6ZeI=
kumoly.io/lib/ksrv v0.1.4/go.mod h1:eKfhJR5mOqlQZAy5EUI+Avfxirx2/nNW79r+Ki2C18k=

View File

@ -8,6 +8,8 @@ import (
"os"
"path/filepath"
"strconv"
"kumoly.io/lib/ksrv"
)
func (cui *ConfigUI) ListFiles(w http.ResponseWriter, r *http.Request) {
@ -22,7 +24,7 @@ func (cui *ConfigUI) GetFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
ksrv.Response(w, 404, []byte("file not found"))
return
}
data, err := file.Read()
@ -55,7 +57,7 @@ func (cui *ConfigUI) GetDelta(w http.ResponseWriter, r *http.Request) {
}
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
ksrv.Response(w, 404, []byte("file not found"))
return
}
f, err := os.Open(file.Path)
@ -106,7 +108,7 @@ func (cui *ConfigUI) PostFile(w http.ResponseWriter, r *http.Request) {
}
file, err := cui.File(f.Name)
if err != nil {
response(w, 404, []byte("file not found"))
ksrv.Response(w, 404, []byte("file not found"))
return
}
if err := file.Write([]byte(f.Data)); err != nil {
@ -119,7 +121,7 @@ func (cui *ConfigUI) Apply(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
ksrv.Response(w, 404, []byte("file not found"))
return
}
result, err := file.Do(cui.cmdTimeout)
@ -145,7 +147,7 @@ func (cui *ConfigUI) Download(w http.ResponseWriter, r *http.Request) {
}
file, err := cui.File(name)
if err != nil {
response(w, 404, []byte("file not found"))
ksrv.Response(w, 404, []byte("file not found"))
return
}
data, err := file.Read()

1
log.go
View File

@ -1 +0,0 @@
package configui

View File

@ -1,69 +0,0 @@
package configui
import (
"bytes"
"log"
"net/http"
"strconv"
)
type CuiResponseWriter struct {
http.ResponseWriter
StatueCode int
}
func (w *CuiResponseWriter) WriteHeader(statusCode int) {
if w.StatueCode != 0 {
return
}
w.StatueCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *CuiResponseWriter) Write(body []byte) (int, error) {
if w.StatueCode == 0 {
w.WriteHeader(200)
}
return w.ResponseWriter.Write(body)
}
func response(w http.ResponseWriter, status int, body []byte) (int, error) {
w.WriteHeader(status)
return w.Write(body)
}
func abort(w http.ResponseWriter, err interface{}) (int, error) {
log.Println(err)
switch v := err.(type) {
case int:
w.WriteHeader(v)
return w.Write([]byte(strconv.Itoa(v)))
case string:
w.WriteHeader(500)
return w.Write([]byte(v))
case error:
w.WriteHeader(500)
return w.Write([]byte(v.Error()))
default:
w.WriteHeader(500)
return w.Write([]byte(strconv.Itoa(500)))
}
}
func catch(rw *CuiResponseWriter, r *http.Request) {
ex := recover()
if ex != nil {
abort(rw, ex)
log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
}
}
func (cui *ConfigUI) Parse(w http.ResponseWriter, name string, data interface{}) error {
buf := &bytes.Buffer{}
err := cui.tmpl.ExecuteTemplate(buf, "home", data)
if err != nil {
panic(err)
}
_, err = w.Write(buf.Bytes())
return err
}

View File

@ -1,9 +1,10 @@
package configui
import (
"log"
"net/http"
"strings"
"kumoly.io/lib/ksrv"
)
func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -11,25 +12,28 @@ func (cui *ConfigUI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (cui *ConfigUI) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// log.Println(r.URL)
rw := &CuiResponseWriter{w, 0}
defer catch(rw, r)
ip := GetIP(r)
k := ksrv.New()
cui.ksrv_log = k.GetLogger()
cui.setLog()
k.SetNoLogCondition(func(r *http.Request) bool {
if strings.HasPrefix(r.URL.Path, "/public") || r.URL.Query().Get("f") == "true" {
return true
}
return false
})
return k.Middleware(
http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if cui.AllowIP != "" {
if !matchIPGlob(ip, cui.AllowIP) {
ip := ksrv.GetIP(r)
if !ksrv.MatchIPGlob(ip, cui.AllowIP) {
rw.WriteHeader(403)
panic("permission denied")
}
}
next.ServeHTTP(rw, r)
//logging
if !strings.HasPrefix(r.URL.Path, "/public") && r.URL.Query().Get("f") != "true" {
log.Printf("%s %s %d %s %s\n", ip, r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
}
},
}),
)
}
func (cui *ConfigUI) mux() *http.ServeMux {

62
util.go
View File

@ -4,55 +4,10 @@ import (
"archive/tar"
"compress/gzip"
"io"
"net"
"net/http"
"os"
"strings"
)
func matchIPGlob(ip, pattern string) bool {
parts := strings.Split(pattern, ".")
seg := strings.Split(ip, ".")
for i, part := range parts {
// normalize pattern to 3 digits
switch len(part) {
case 1:
if part == "*" {
part = "***"
} else {
part = "00" + part
}
case 2:
if strings.HasPrefix(part, "*") {
part = "*" + part
} else if strings.HasSuffix(part, "*") {
part = part + "*"
} else {
part = "0" + part
}
}
// normalize ip to 3 digits
switch len(seg[i]) {
case 1:
seg[i] = "00" + seg[i]
case 2:
seg[i] = "0" + seg[i]
}
for j := range part {
if string(part[j]) == "*" {
continue
}
if part[j] != seg[i][j] {
return false
}
}
}
return true
}
func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error {
gw := gzip.NewWriter(buf)
defer gw.Close()
@ -110,20 +65,3 @@ func bundle(buf io.Writer, paths []string, rootDir string, flat bool) error {
return nil
}
func GetIP(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {
ips := r.Header.Get("X-Forwarded-For")
ipArr := strings.Split(ips, ",")
ip = strings.Trim(ipArr[len(ipArr)-1], " ")
}
if ip == "" {
var err error
ip, _, err = net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
}
return ip
}