diff --git a/.github/workflows/build-go-binary.yml b/.github/workflows/build-go-binary.yml index e522452..2aaae32 100644 --- a/.github/workflows/build-go-binary.yml +++ b/.github/workflows/build-go-binary.yml @@ -23,4 +23,4 @@ jobs: goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} binary_name: "moon-counter" - extra_files: LICENSE readme.md config.yaml + extra_files: LICENSE config.yaml diff --git a/.gitignore b/.gitignore index adb36c8..afe0410 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.exe \ No newline at end of file +*.exe +*.db \ No newline at end of file diff --git a/common/logger.go b/common/logger.go index a3cbae9..0d75197 100644 --- a/common/logger.go +++ b/common/logger.go @@ -4,6 +4,14 @@ import ( "log" ) +type Logger struct { + path string +} + +func (l Logger) InitLogger(path string) { + +} + func SilentError(v ...any) { log.Println(v...) } diff --git a/common/models.go b/common/models.go index 40ded53..c4bd262 100644 --- a/common/models.go +++ b/common/models.go @@ -2,10 +2,11 @@ package common type Config struct { Host string `yaml:"host"` - Port int `yaml:"port"` - ImgTheme string `yaml:"imgTheme"` + Port int `yaml:"listen"` + ImgTheme string `yaml:"img_yheme"` Cors bool `yaml:"cors"` Hostnames []string `yaml:"hostnames"` + ErrorLog string `yaml:"error_log"` DBCfg DBConfig `yaml:"db"` } diff --git a/config.yaml b/config.yaml index 54d6216..3799825 100644 --- a/config.yaml +++ b/config.yaml @@ -1,12 +1,16 @@ # Moon-Counter Config -# See config docs: +# See config docs: https://mini.moonlab.top/post/20231224-14/ host: localhost:3800 -port: 3800 +listen: 3800 cors: true -imgTheme: rule34 +img_theme: rule34 hostnames: - moonlab.top + # Remove localhost in production - localhost + +error_log: error.log + db: type: sqlite dbname: moon.db diff --git a/main.go b/main.go index 5d5526b..b6f540e 100644 --- a/main.go +++ b/main.go @@ -33,7 +33,7 @@ func main() { db.InitDB(config.DBCfg.Dbname) defer db.CloseDB() - server.LoadAssets("rule34") + server.LoadAssets(config.ImgTheme) s := server.NewInstance(&config, db) s.Start() diff --git a/moon.db b/moon.db index ed083b9..36470ba 100644 Binary files a/moon.db and b/moon.db differ diff --git a/moon.js b/moon.js new file mode 100644 index 0000000..9a0592d --- /dev/null +++ b/moon.js @@ -0,0 +1,23 @@ +// Text +fetch('//%s/counter/text') + .then(r => { + return r.text(); + }) + .then(d => { + document.getElementById("moon-counter").innerText = d; + }) + .catch(e => { + console.error(e); + }); + +// Img +fetch('//%s/counter/img') + .then(r => { + return r.text(); + }) + .then(d => { + document.getElementById("moon-counter-img").src='data:image/svg+xml,' + d; + }) + .catch(e => { + console.error(e); + }); \ No newline at end of file diff --git a/readme.md b/readme.md index 03ef745..57114ca 100644 --- a/readme.md +++ b/readme.md @@ -1,34 +1,46 @@ # Moon-Counter +English | [中文](readme_cn.md) + A fast, simple & easy-to-use webpage visitor counter, but not only limited to websites. With a visual admin panel, put Moon-Counter at every corner -🚀 Fast and Simple +##### 🚀 Fast and Simple -🎉 Self-Host & Easy-To-Use -Deploy with only one file, zero dependency. No annoying complex installation +##### 🎉 Easy Deployment -🔒 Secure -Support [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), make it hard for strangers to use your self-host service without permisson +Run counter server with only one binary file, zero dependency. No annoying complex installation -🌟 SQLite Database. -Reeeallly easy to control and move +##### 🔒 Secure [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) Support + + Make it hard for strangers to use your counter service without permisson to tally for them -Two modes to choose, text & image +##### 🌟 SQLite Database. + +Reeeallly easy to control and move ## Image Counter +#### Common Method +Make sure id argument is unique for every webpage + ``` -# Markdown style -# Make sure id arg is unique for each webpage # You can use this in Github Profile -![]()[https://yoursite.com/counter/img?id=uniqueID] +![](https://yoursite.com/counter/img?id=uniqueID) -# HTML style -# Unique id arg is automatically handled -# If cors is on, you should only use image counter in this way - + +``` + +#### Secure CORS + +Unique id arg is automatically handled + +If cors is on in the config file, server will check whether the request origin is vaild, and return cors resources. +In this case, you should only use image counter in this way + +``` + ``` @@ -37,7 +49,7 @@ Two modes to choose, text & image Add the following code to where you wanna place a text counter. ``` - + ``` @@ -63,12 +75,12 @@ nano config.yaml $ moon-counter ``` -For more details and configuration help, Please visit [my blog](https://mini.moonlab.top/) +For more details and configuration help, Please visit [my blog](https://mini.moonlab.top/post/20231224-14/) # Credits -# Lisence +# License MIT diff --git a/readme_cn.md b/readme_cn.md new file mode 100644 index 0000000..e051918 --- /dev/null +++ b/readme_cn.md @@ -0,0 +1,56 @@ +# Moon-Counter + +[English]((readme.md)) | 中文 + +快速,简单 & 易于使用的网页浏览量计数,但并不只局限于网站。 + +##### 🚀 Fast and Simple + +##### 🎉 部署简单 + +只需一个二进制文件即可启动计数服务器,零依赖。没有繁琐的安装过程。 + +##### 🔒 安全的 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 支持 + +让陌生人难以私自使用你的计数服务器,来为他们计数 + +##### 🌟 SQLite Database. + +易于控制和搬迁 + +## 图片计数器 + +#### 普通方法 + +请确保每个网页的 id 参数是独一无二的 + +``` +# You can use this in Github Profile +![](https://yoursite.com/counter/img?id=uniqueID) + + +``` + +#### 安全 CORS + +Unique id 参数会自动被处理 + +如果在配置文件中启用了 CORS,服务器将检查请求的来源是否合法,并返回 CORS 资源。在这种情况下,你应该仅以这种方式使用图片计数器。 + +``` + + +``` + +## 文字计数器 + +将以下 html 代码放在你想要计数的地方 + +``` + + +``` + +# 许可协议 + +MIT diff --git a/server/factory.go b/server/factory.go index eab8280..37d5d21 100644 --- a/server/factory.go +++ b/server/factory.go @@ -70,12 +70,14 @@ func LoadAssets(theme string) { } func BuildCounterImg(c string) string { - log.Println("c:", c) + iTimes := 6 - len(c) + for i := 0; i < iTimes; i++ { + c = "0" + c + } var numberTempletes string // Todo: Handle a situation if each image's dimentions are different for i, sDigit := range c { digit := strings.Index("0123456789", string(sDigit)) - log.Println("d:", digit) img := images[digit] // Todo: Add more image type support, exclude .gif numberTempletes += fmt.Sprintf(NUMBER_TEMPLATE, i*img.width, img.width, img.height, "gif", *img.data) diff --git a/server/helper.go b/server/helper.go index ac5d57a..643ecdd 100644 --- a/server/helper.go +++ b/server/helper.go @@ -7,47 +7,19 @@ import ( "github.com/HelloLingC/moon-counter/common" ) -const JS = ` -fetch('//%s/counter/text') - .then(r => { - return r.text(); - }) - .then(d => { - document.getElementById("moon-counter").innerText = d; - }) - .catch(e => { - console.error(e); - }); -` +const JS = `fetch("//%s/counter/text").then(e=>e.text()).then(e=>{document.getElementById("moon-counter").innerText=e}).catch(e=>{console.error(e)});` -const JS_IMG = ` -fetch('//%s/counter/img') - .then(r => { - return r.text(); - }) - .then(d => { - document.getElementById("moon-counter").innerText = d; - }) - .catch(e => { - console.error(e); - }); -` +const JS_IMG = `fetch("//%s/counter/img").then(e=>e.text()).then(e=>{document.getElementById("moon-counter-img").src="data:image/svg+xml,"+e}).catch(e=>{console.error(e)});` -func checkOrigin(w http.ResponseWriter, origin string, hostnames []string) string { +func checkOrigin(w http.ResponseWriter, origin string, hostnames []string) bool { parsed, err := url.Parse(origin) if err != nil { http.Error(w, "Invaild orgin: not a url", http.StatusBadRequest) - return "" + return false } isAllowed := common.StrIsInSlice(parsed.Hostname(), hostnames) if !isAllowed { - http.Error(w, "Forbidden", http.StatusForbidden) - return "" + http.Error(w, "Invaild orgin: Forbidden", http.StatusForbidden) } - rmOrigin, err := common.StrRemoveProtocol(origin) - if err != nil { - http.Error(w, "Invaild origin: missing protocal", http.StatusBadRequest) - return "" - } - return rmOrigin + return isAllowed } diff --git a/server/server.go b/server/server.go index f9124f0..465abc1 100644 --- a/server/server.go +++ b/server/server.go @@ -23,13 +23,8 @@ func NewInstance(config *common.Config, db database.IDatabase) *Server { func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - // Handle preflight requests - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusNoContent) - return - } next.ServeHTTP(w, r) }) } @@ -45,28 +40,44 @@ func (s Server) jsImgHndl(w http.ResponseWriter, r *http.Request) { } func (s Server) imgHndl(w http.ResponseWriter, r *http.Request) { - identifier := r.URL.Query().Get("id") - if identifier == "" { - http.Error(w, "missing identifier", http.StatusBadRequest) + origin := r.Header.Get("Origin") + if s.Config.Cors && !checkOrigin(w, origin, s.Config.Hostnames) { return } + var identifier string + if origin != "" { + // Client is using JS to request here + identifier = origin + } else { + identifier = r.URL.Query().Get("id") + if identifier == "" { + http.Error(w, "missing identifier", http.StatusBadRequest) + return + } + } count, err := s.DB.AddCounter(identifier) if err != nil { common.SilentError("SQL err when adding:", err) http.Error(w, "DB Error", http.StatusServiceUnavailable) return } + // Todo: digits customization svg := BuildCounterImg(fmt.Sprintf("%d", count)) + w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Content-Type", "image/svg+xml") fmt.Fprint(w, svg) } func (s Server) textHndl(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - rmOrigin := checkOrigin(w, origin, s.Config.Hostnames) - if rmOrigin == "" { + if s.Config.Cors && !checkOrigin(w, origin, s.Config.Hostnames) { // Didn't pass the origin check - http.Error(w, "access blocked", http.StatusForbidden) + return + } + // Todo; text counter support id argument + rmOrigin, err := common.StrRemoveProtocol(origin) + if err != nil { + http.Error(w, "Invaild origin: missing protocal", http.StatusBadRequest) return } count, err := s.DB.AddCounter(rmOrigin) @@ -75,7 +86,7 @@ func (s Server) textHndl(w http.ResponseWriter, r *http.Request) { http.Error(w, "DB error", http.StatusServiceUnavailable) return } - // [S] Do NOT send all the allowed origins to the client + // Do NOT send all the allowed origins to the client w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Content-Type", "text/plain") fmt.Fprintf(w, "%d", count) @@ -85,10 +96,12 @@ func (s Server) Start() { log.Printf("Moon Counter starts running at localhost:%d", s.Config.Port) tHndl := http.HandlerFunc(s.textHndl) + iHndl := http.HandlerFunc(s.imgHndl) http.HandleFunc("/moon-counter/js", s.jsTextHndl) + http.HandleFunc("/moon-counter/js/img", s.jsImgHndl) http.Handle("/counter/text", corsMiddleware(tHndl)) - http.HandleFunc("/counter/img", s.imgHndl) + http.Handle("/counter/img", corsMiddleware(iHndl)) err := http.ListenAndServe(fmt.Sprintf(":%d", s.Config.Port), nil) if err != nil { diff --git a/server/server_admin.go b/server/server_admin.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/server/server_admin.go @@ -0,0 +1 @@ +package server