Compare commits
3 Commits
1d7b8f5868
...
4ebb686b1c
Author | SHA1 | Date |
---|---|---|
Evan Chen | 4ebb686b1c | |
Evan Chen | 4bcce8ddfd | |
Evan Chen | f654cd347c |
|
@ -1 +1,2 @@
|
||||||
dist
|
dist
|
||||||
|
node_modules
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
dist
|
dist
|
||||||
|
node_modules
|
||||||
|
public/css/main.css*
|
||||||
|
public/js/main.js*
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
42
api.go
42
api.go
|
@ -11,7 +11,7 @@ import (
|
||||||
func ListFiles(w http.ResponseWriter, r *http.Request) {
|
func ListFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := json.Marshal(files)
|
data, err := json.Marshal(files)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
|
@ -21,12 +21,12 @@ func GetFile(w http.ResponseWriter, r *http.Request) {
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
file, ok := files[name]
|
file, ok := files[name]
|
||||||
if name == "" || !ok {
|
if name == "" || !ok {
|
||||||
HttpWriter(w, 404, "file not found")
|
MakeResponse(w, 404, []byte("file not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := file.Read()
|
data, err := file.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response, err := json.Marshal(map[string]string{
|
response, err := json.Marshal(map[string]string{
|
||||||
|
@ -36,7 +36,7 @@ func GetFile(w http.ResponseWriter, r *http.Request) {
|
||||||
"data": string(data),
|
"data": string(data),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -47,21 +47,21 @@ func PostFile(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f := configui.File{}
|
f := configui.File{}
|
||||||
if err := json.Unmarshal(data, &f); err != nil {
|
if err := json.Unmarshal(data, &f); err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, ok := files[f.Alias]
|
file, ok := files[f.Alias]
|
||||||
if !ok {
|
if !ok {
|
||||||
HttpWriter(w, 404, "file not found")
|
MakeResponse(w, 404, []byte("file not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := file.Write([]byte(f.Data)); err != nil {
|
if err := file.Write([]byte(f.Data)); err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
|
@ -71,12 +71,12 @@ func Apply(w http.ResponseWriter, r *http.Request) {
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
file, ok := files[name]
|
file, ok := files[name]
|
||||||
if name == "" || !ok {
|
if name == "" || !ok {
|
||||||
HttpWriter(w, 404, "file not found")
|
MakeResponse(w, 404, []byte("file not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := file.Do()
|
result, err := file.Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte(result))
|
w.Write([]byte(result))
|
||||||
|
@ -95,27 +95,31 @@ func LoadConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ftmp, err := configui.ReadConfig(string(data))
|
ftmp, err := configui.ReadConfig(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files = configui.GetFileMap(ftmp)
|
files = configui.GetFileMap(ftmp)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
func getConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
config := []configui.File{}
|
data, err := GetConfig()
|
||||||
for _, f := range files {
|
|
||||||
config = append(config, *f)
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HttpWriter(w, 500, err.Error())
|
AbortError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetConfig() ([]byte, error) {
|
||||||
|
config := []configui.File{}
|
||||||
|
for _, f := range files {
|
||||||
|
config = append(config, *f)
|
||||||
|
}
|
||||||
|
return json.Marshal(config)
|
||||||
|
}
|
||||||
|
|
25
main.go
25
main.go
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -13,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"kumoly.io/tools/configui/configui"
|
"kumoly.io/tools/configui/configui"
|
||||||
|
"kumoly.io/tools/configui/public"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates
|
//go:embed templates
|
||||||
|
@ -27,6 +27,7 @@ var (
|
||||||
|
|
||||||
flagBind string
|
flagBind string
|
||||||
flagLogFile string
|
flagLogFile string
|
||||||
|
flagAllow string
|
||||||
flagVer bool
|
flagVer bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ func init() {
|
||||||
flag.StringVar(&flagAlias, "n", "", "alias of file")
|
flag.StringVar(&flagAlias, "n", "", "alias of file")
|
||||||
flag.StringVar(&flagAction, "c", "", "cmd to apply")
|
flag.StringVar(&flagAction, "c", "", "cmd to apply")
|
||||||
flag.StringVar(&flagLogFile, "log", "", "log to file")
|
flag.StringVar(&flagLogFile, "log", "", "log to file")
|
||||||
|
flag.StringVar(&flagAllow, "allow", "", "IPs to allow, blank to allow all")
|
||||||
flag.StringVar(&flagBind, "bind", "localhost:8000", "address to bind")
|
flag.StringVar(&flagBind, "bind", "localhost:8000", "address to bind")
|
||||||
flag.BoolVar(&flagVer, "v", false, "show version")
|
flag.BoolVar(&flagVer, "v", false, "show version")
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
|
@ -96,7 +98,7 @@ func main() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/conf", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
GetConfig(w, r)
|
getConfigHandler(w, r)
|
||||||
} else if r.Method == "POST" {
|
} else if r.Method == "POST" {
|
||||||
LoadConfig(w, r)
|
LoadConfig(w, r)
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,28 +135,15 @@ func main() {
|
||||||
w.WriteHeader(404)
|
w.WriteHeader(404)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
err := tmpl.ExecuteTemplate(buf, "home", nil)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(500)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
} else {
|
|
||||||
w.Write(buf.Bytes())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
mux.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.FS(public.FS))))
|
||||||
tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
|
tmpl = template.Must(template.New("").ParseFS(tmplFS, "templates/*.tmpl", "templates/**/*.tmpl"))
|
||||||
|
setRoutes(mux)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: flagBind,
|
Addr: flagBind,
|
||||||
WriteTimeout: time.Second * 3,
|
WriteTimeout: time.Second * 3,
|
||||||
ReadTimeout: time.Second * 30,
|
ReadTimeout: time.Second * 30,
|
||||||
Handler: LogMiddleware(mux),
|
Handler: Middleware(mux),
|
||||||
}
|
}
|
||||||
|
|
||||||
// start server
|
// start server
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
StatueCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
if w.StatueCode != 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.StatueCode = statusCode
|
||||||
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *ResponseWriter) Write(body []byte) (int, error) {
|
||||||
|
if w.StatueCode == 0 {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
return w.ResponseWriter.Write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeResponse(w http.ResponseWriter, status int, body []byte) (int, error) {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
return w.Write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AbortError(w http.ResponseWriter, err interface{}) (int, error) {
|
||||||
|
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 *ResponseWriter, r *http.Request) {
|
||||||
|
ex := recover()
|
||||||
|
if ex != nil {
|
||||||
|
AbortError(rw, r)
|
||||||
|
log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Middleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rw := &ResponseWriter{w, 0}
|
||||||
|
defer Catch(rw, r)
|
||||||
|
abort := false
|
||||||
|
if flagAllow != "" {
|
||||||
|
if !matchIPGlob(GetIP(r), flagAllow) {
|
||||||
|
MakeResponse(rw, 403, []byte("permission denyed"))
|
||||||
|
abort = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !abort {
|
||||||
|
next.ServeHTTP(rw, r)
|
||||||
|
}
|
||||||
|
log.Printf("%s %s %d %s %s\n", GetIP(r), r.Method, rw.StatueCode, r.URL, r.Header.Get("User-Agent"))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(w http.ResponseWriter, name string, data interface{}) error {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := tmpl.ExecuteTemplate(buf, "home", data)
|
||||||
|
if err != nil {
|
||||||
|
AbortError(w, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(buf.Bytes())
|
||||||
|
return err
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "configui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "a web app to edit and action on update",
|
||||||
|
"scripts": {
|
||||||
|
"build-sass": "sass --style compressed src/main.scss public/css/main.css",
|
||||||
|
"build-js": "parcel build src/main.js --dist-dir public/js",
|
||||||
|
"start":"npm run build-sass && npm run build-js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git@kumoly.io:tools/configui.git"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bulma": "^0.9.3",
|
||||||
|
"prismjs": "^1.25.0",
|
||||||
|
"sass": "^1.43.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@parcel/transformer-sass": "^2.0.0",
|
||||||
|
"parcel": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package public
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed js css
|
||||||
|
var FS embed.FS
|
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setRoutes(mux *http.ServeMux) {
|
||||||
|
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Files := []string{}
|
||||||
|
for file := range files {
|
||||||
|
Files = append(Files, files[file].Alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentB, err := GetConfig()
|
||||||
|
content := ""
|
||||||
|
if err != nil {
|
||||||
|
content = err.Error()
|
||||||
|
} else {
|
||||||
|
content = string(contentB)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := struct {
|
||||||
|
Active string
|
||||||
|
Files []string
|
||||||
|
Content string
|
||||||
|
Lang string
|
||||||
|
}{
|
||||||
|
Files: Files,
|
||||||
|
Active: "ConfigUI",
|
||||||
|
Content: content,
|
||||||
|
Lang: "json",
|
||||||
|
}
|
||||||
|
|
||||||
|
Parse(w, "home", data)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Prism from "prismjs";
|
||||||
|
import "prismjs/plugins/custom-class/prism-custom-class";
|
||||||
|
|
||||||
|
|
||||||
|
Prism.plugins.customClass.map({ number: "prism-number", tag: "prism-tag" });
|
|
@ -0,0 +1,2 @@
|
||||||
|
@charset "utf-8";
|
||||||
|
@import "../node_modules/bulma/bulma.sass";
|
|
@ -0,0 +1,233 @@
|
||||||
|
/* Code-Input Compability */
|
||||||
|
/* By WebCoder49 */
|
||||||
|
/* First Published on CSS-Tricks.com */
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
border: none;
|
||||||
|
overflow: auto;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
resize: none; /*remove the resize handle on the bottom right*/
|
||||||
|
}
|
||||||
|
.content .tag, .content .number {
|
||||||
|
display: inline;
|
||||||
|
padding: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
text-align: inherit;
|
||||||
|
vertical-align: inherit;
|
||||||
|
border-radius: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
background: inherit;
|
||||||
|
margin: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Please see the article */
|
||||||
|
|
||||||
|
#editing, #highlighting {
|
||||||
|
/* Both elements need the same text and space styling so they are directly on top of each other */
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 0;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
#editing, #highlighting, #highlighting * {
|
||||||
|
/* Also add text styles to highlighing tokens */
|
||||||
|
font-size: 15pt;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 20pt;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#editing, #highlighting {
|
||||||
|
/* In the same place */
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Move the textarea in front of the result */
|
||||||
|
|
||||||
|
#editing {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
#highlighting {
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Make textarea almost completely transparent */
|
||||||
|
|
||||||
|
#editing {
|
||||||
|
color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
caret-color: white; /* Or choose your favourite color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Can be scrolled */
|
||||||
|
#editing, #highlighting {
|
||||||
|
overflow: auto;
|
||||||
|
white-space: nowrap; /* Allows textarea to scroll horizontally */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No resize on textarea */
|
||||||
|
#editing {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paragraphs; First Image */
|
||||||
|
* {
|
||||||
|
font-family: "Fira Code", monospace;
|
||||||
|
}
|
||||||
|
p code {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #eee;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Syntax Highlighting from prism.js starts below, partly modified: */
|
||||||
|
|
||||||
|
/* PrismJS 1.23.0
|
||||||
|
https://prismjs.com/download.html#themes=prism-funky&languages=markup */
|
||||||
|
/**
|
||||||
|
* prism.js Funky theme
|
||||||
|
* Based on “Polyfilling the gaps” talk slides http://lea.verou.me/polyfilling-the-gaps/
|
||||||
|
* @author Lea Verou
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: .4em .8em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
/* background: url('data:image/svg+xml;charset=utf-8,<svg%20version%3D"1.1"%20xmlns%3D"http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg"%20width%3D"100"%20height%3D"100"%20fill%3D"rgba(0%2C0%2C0%2C.2)">%0D%0A<polygon%20points%3D"0%2C50%2050%2C0%200%2C0"%20%2F>%0D%0A<polygon%20points%3D"0%2C100%2050%2C100%20100%2C50%20100%2C0"%20%2F>%0D%0A<%2Fsvg>');
|
||||||
|
background-size: 1em 1em; - WebCoder49*/
|
||||||
|
background: black; /* - WebCoder49 */
|
||||||
|
}
|
||||||
|
|
||||||
|
code[class*="language-"] {
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
box-shadow: -.3em 0 0 .3em black, .3em 0 0 .3em black;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .2em;
|
||||||
|
border-radius: .3em;
|
||||||
|
box-shadow: none;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol {
|
||||||
|
color: #0cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.token.variable,
|
||||||
|
.token.inserted {
|
||||||
|
color: yellowgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: deeppink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.deleted {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plugin styles: Diff Highlight */
|
||||||
|
pre.diff-highlight.diff-highlight > code .token.deleted:not(.prefix),
|
||||||
|
pre > code.diff-highlight.diff-highlight .token.deleted:not(.prefix) {
|
||||||
|
background-color: rgba(255, 0, 0, .3);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.diff-highlight.diff-highlight > code .token.inserted:not(.prefix),
|
||||||
|
pre > code.diff-highlight.diff-highlight .token.inserted:not(.prefix) {
|
||||||
|
background-color: rgba(0, 255, 128, .3);
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* End of prism.js syntax highlighting*/
|
|
@ -0,0 +1,39 @@
|
||||||
|
// codeInput
|
||||||
|
// by WebCoder49
|
||||||
|
// Based on a CSS-Tricks Post
|
||||||
|
// Needs Prism.js
|
||||||
|
function update(text) {
|
||||||
|
let result_element = document.querySelector("#highlighting-content");
|
||||||
|
// Handle final newlines (see article)
|
||||||
|
if(text[text.length-1] == "\n") {
|
||||||
|
text += " ";
|
||||||
|
}
|
||||||
|
// Update code
|
||||||
|
result_element.innerHTML = text.replace(new RegExp("&", "g"), "&").replace(new RegExp("<", "g"), "<"); /* Global RegExp */
|
||||||
|
// Syntax Highlight
|
||||||
|
Prism.highlightElement(result_element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync_scroll(element) {
|
||||||
|
/* Scroll result to scroll coords of event - sync with textarea */
|
||||||
|
let result_element = document.querySelector("#highlighting");
|
||||||
|
// Get and set x and y
|
||||||
|
result_element.scrollTop = element.scrollTop;
|
||||||
|
result_element.scrollLeft = element.scrollLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_tab(element, event) {
|
||||||
|
let code = element.value;
|
||||||
|
if(event.key == "Tab") {
|
||||||
|
/* Tab key pressed */
|
||||||
|
event.preventDefault(); // stop normal
|
||||||
|
let before_tab = code.slice(0, element.selectionStart); // text before tab
|
||||||
|
let after_tab = code.slice(element.selectionEnd, element.value.length); // text after tab
|
||||||
|
let cursor_pos = element.selectionEnd + 1; // where cursor moves after tab - moving forward by 1 char to after tab
|
||||||
|
element.value = before_tab + "\t" + after_tab; // add tab char
|
||||||
|
// move cursor
|
||||||
|
element.selectionStart = cursor_pos;
|
||||||
|
element.selectionEnd = cursor_pos;
|
||||||
|
update(element.value); // Update text to include indent
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
{{define "base/footer"}}
|
{{define "base/footer"}}
|
||||||
<script src="/public/js/main.js"></script>
|
<script src="/public/js/main.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
<!-- <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> -->
|
||||||
|
|
||||||
|
<script src="public/js/prism1.23.0.min.js"></script>
|
||||||
|
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/prism.min.js"></script> -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
|
@ -4,8 +4,10 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Serviced</title>
|
<title>ConfigUI</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
<link rel="stylesheet" href="/public/css/bulma0.9.3.min.css">
|
||||||
|
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"> -->
|
||||||
|
<link rel="stylesheet" href="/public/css/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{end}}
|
{{end}}
|
|
@ -1,14 +1,54 @@
|
||||||
{{define "home"}}
|
{{define "home"}}
|
||||||
{{template "base/header" .}}
|
{{template "base/header" .}}
|
||||||
<section class="section">
|
|
||||||
<div class="container">
|
<div class="columns is-mobile is-centered">
|
||||||
<h1 class="title">
|
<div class="column is-half">
|
||||||
Hello World
|
<section class="section">
|
||||||
</h1>
|
<container class="container is-max-desktop">
|
||||||
<p class="subtitle">
|
<h1 class="title">ConfigUI</h1>
|
||||||
My first website with <strong>Bulma</strong>!
|
</container>
|
||||||
</p>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
|
<div class="tile is-ancestor mx-2">
|
||||||
|
<div class="tile is-3 is-vertical is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
|
<section class="is-large">
|
||||||
|
<aside class="menu">
|
||||||
|
<p class="menu-label">
|
||||||
|
Files
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
{{ range .Files }}
|
||||||
|
<li><a>{{ . }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
<p class="menu-label">
|
||||||
|
ConfigUI
|
||||||
|
</p>
|
||||||
|
<ul class="menu-list">
|
||||||
|
<li><a{{if eq .Active "ConfigUI"}} class="is-active"{{end}}>config.json</a></li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-child box">
|
||||||
|
<p class="title">Two</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile is-parent">
|
||||||
|
<div class="tile is-child box">
|
||||||
|
<p class="title">Three</p>
|
||||||
|
<textarea class="content" placeholder="Enter HTML Source Code" id="editing" spellcheck="false" oninput="update(this.value); sync_scroll(this);" onscroll="sync_scroll(this);" onkeydown="check_tab(this, event);"></textarea>
|
||||||
|
<pre id="highlighting" aria-hidden="true">
|
||||||
|
<code class="language-html" id="highlighting-content">{{.Content}}</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<!-- <code-input class="content" id="edit" lang="{{.Lang}}" name="config">{{.Content}}</code-input> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
{{end}}
|
{{end}}
|
60
util.go
60
util.go
|
@ -4,26 +4,51 @@ import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LogMiddleware(next http.Handler) http.Handler {
|
func matchIPGlob(ip, pattern string) bool {
|
||||||
return http.HandlerFunc(
|
parts := strings.Split(pattern, ".")
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
seg := strings.Split(ip, ".")
|
||||||
defer func() {
|
for i, part := range parts {
|
||||||
r := recover()
|
|
||||||
if r != nil {
|
// normalize pattern to 3 digits
|
||||||
log.Println("panic", r)
|
switch len(part) {
|
||||||
w.WriteHeader(500)
|
case 1:
|
||||||
|
if part == "*" {
|
||||||
|
part = "***"
|
||||||
|
} else {
|
||||||
|
part = "00" + part
|
||||||
}
|
}
|
||||||
}()
|
case 2:
|
||||||
next.ServeHTTP(w, r)
|
if strings.HasPrefix(part, "*") {
|
||||||
log.Printf("%s %s %s %s\n", r.RemoteAddr, r.Method, r.URL, r.Header.Get("User-Agent"))
|
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, flat bool) error {
|
func bundle(buf io.Writer, paths []string, flat bool) error {
|
||||||
|
@ -83,8 +108,3 @@ func bundle(buf io.Writer, paths []string, flat bool) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HttpWriter(w http.ResponseWriter, status int, errStr string) {
|
|
||||||
w.WriteHeader(status)
|
|
||||||
w.Write([]byte(errStr))
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue