2016-06-28 17:35:48 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha1"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2016-07-05 13:48:57 +00:00
|
|
|
"io/ioutil"
|
2016-06-29 13:13:26 +00:00
|
|
|
"log"
|
2016-06-28 17:35:48 +00:00
|
|
|
"net/http"
|
2016-06-28 22:52:49 +00:00
|
|
|
"net/url"
|
2016-07-05 13:48:57 +00:00
|
|
|
"os"
|
2016-06-28 21:56:22 +00:00
|
|
|
"sort"
|
2016-06-28 17:35:48 +00:00
|
|
|
"strings"
|
|
|
|
"text/template"
|
2016-06-28 22:51:51 +00:00
|
|
|
"time"
|
|
|
|
|
2016-07-05 13:48:57 +00:00
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
|
2016-06-28 22:51:51 +00:00
|
|
|
"golang.org/x/net/context"
|
2016-06-28 17:35:48 +00:00
|
|
|
|
2016-06-29 13:13:26 +00:00
|
|
|
"github.com/Luzifer/badge-gen/cache"
|
2021-03-06 22:54:51 +00:00
|
|
|
"github.com/Luzifer/go_helpers/v2/accessLogger"
|
|
|
|
"github.com/Luzifer/rconfig/v2"
|
2016-06-28 17:35:48 +00:00
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/tdewolff/minify"
|
|
|
|
"github.com/tdewolff/minify/svg"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
xSpacing = 8
|
|
|
|
defaultColor = "4c1"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
cfg = struct {
|
2016-07-05 13:48:57 +00:00
|
|
|
Port int64 `env:"PORT"`
|
|
|
|
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
|
|
|
|
Cache string `flag:"cache" default:"mem://" description:"Where to cache query results from thirdparty APIs"`
|
|
|
|
ConfStorage string `flag:"config" default:"config.yaml" description:"Configuration store"`
|
2016-06-28 17:35:48 +00:00
|
|
|
}{}
|
2016-07-05 13:48:57 +00:00
|
|
|
|
2016-06-28 21:56:22 +00:00
|
|
|
serviceHandlers = map[string]serviceHandler{}
|
2016-06-28 22:22:48 +00:00
|
|
|
version = "dev"
|
2016-07-05 13:48:57 +00:00
|
|
|
|
2016-07-05 14:41:48 +00:00
|
|
|
colorList = map[string]string{
|
|
|
|
"brightgreen": "4c1",
|
|
|
|
"green": "97CA00",
|
|
|
|
"yellow": "dfb317",
|
|
|
|
"yellowgreen": "a4a61d",
|
|
|
|
"orange": "fe7d37",
|
|
|
|
"red": "e05d44",
|
|
|
|
"blue": "007ec6",
|
|
|
|
"grey": "555",
|
|
|
|
"gray": "555",
|
|
|
|
"lightgrey": "9f9f9f",
|
|
|
|
"lightgray": "9f9f9f",
|
|
|
|
}
|
|
|
|
|
2016-07-05 13:48:57 +00:00
|
|
|
cacheStore cache.Cache
|
|
|
|
configStore = configStorage{}
|
2016-06-28 17:35:48 +00:00
|
|
|
)
|
|
|
|
|
2016-06-28 21:56:22 +00:00
|
|
|
type serviceHandlerDocumentation struct {
|
|
|
|
ServiceName string
|
|
|
|
DemoPath string
|
|
|
|
Arguments []string
|
|
|
|
Register string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s serviceHandlerDocumentation) DocFormat() string {
|
|
|
|
return "/" + s.Register + "/" + strings.Join(s.Arguments, "/")
|
|
|
|
}
|
|
|
|
|
|
|
|
type serviceHandlerDocumentationList []serviceHandlerDocumentation
|
|
|
|
|
|
|
|
func (s serviceHandlerDocumentationList) Len() int { return len(s) }
|
|
|
|
func (s serviceHandlerDocumentationList) Less(i, j int) bool {
|
2016-07-05 22:28:32 +00:00
|
|
|
return strings.ToLower(s[i].ServiceName) < strings.ToLower(s[j].ServiceName)
|
2016-06-28 21:56:22 +00:00
|
|
|
}
|
|
|
|
func (s serviceHandlerDocumentationList) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
|
|
|
|
type serviceHandler interface {
|
2016-06-29 11:09:08 +00:00
|
|
|
GetDocumentation() serviceHandlerDocumentationList
|
2016-06-28 22:51:51 +00:00
|
|
|
Handle(ctx context.Context, params []string) (title, text, color string, err error)
|
2016-06-28 21:56:22 +00:00
|
|
|
}
|
2016-06-28 17:35:48 +00:00
|
|
|
|
2016-06-28 21:56:22 +00:00
|
|
|
func registerServiceHandler(service string, f serviceHandler) error {
|
2016-06-28 17:35:48 +00:00
|
|
|
if _, ok := serviceHandlers[service]; ok {
|
|
|
|
return errors.New("Duplicate service handler")
|
|
|
|
}
|
|
|
|
serviceHandlers[service] = f
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
rconfig.Parse(&cfg)
|
|
|
|
if cfg.Port != 0 {
|
|
|
|
cfg.Listen = fmt.Sprintf(":%d", cfg.Port)
|
|
|
|
}
|
|
|
|
|
2016-07-05 14:08:43 +00:00
|
|
|
log.Printf("badge-gen %s started...", version)
|
|
|
|
|
2016-06-29 13:13:26 +00:00
|
|
|
var err error
|
|
|
|
cacheStore, err = cache.GetCacheByURI(cfg.Cache)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("Unable to open cache: %s", err)
|
|
|
|
}
|
|
|
|
|
2016-07-05 13:48:57 +00:00
|
|
|
if _, err := os.Stat(cfg.ConfStorage); err == nil {
|
|
|
|
rawConfig, _ := ioutil.ReadFile(cfg.ConfStorage)
|
|
|
|
if err := yaml.Unmarshal(rawConfig, &configStore); err != nil {
|
|
|
|
log.Fatalf("Unable to parse config: %s", err)
|
|
|
|
}
|
2016-07-05 14:08:43 +00:00
|
|
|
log.Printf("Loaded %d value pairs into configuration store", len(configStore))
|
2016-07-05 13:48:57 +00:00
|
|
|
}
|
|
|
|
|
2016-06-28 17:35:48 +00:00
|
|
|
r := mux.NewRouter()
|
|
|
|
r.HandleFunc("/v1/badge", generateBadge).Methods("GET")
|
|
|
|
r.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET")
|
2016-06-28 21:56:22 +00:00
|
|
|
r.HandleFunc("/", handleDemoPage)
|
2016-06-28 17:35:48 +00:00
|
|
|
|
|
|
|
http.ListenAndServe(cfg.Listen, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateServiceBadge(res http.ResponseWriter, r *http.Request) {
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
service := vars["service"]
|
|
|
|
params := strings.Split(vars["parameters"], "/")
|
|
|
|
|
2016-07-07 13:24:33 +00:00
|
|
|
al := accessLogger.New(res)
|
|
|
|
|
2016-06-28 22:51:51 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond)
|
|
|
|
defer cancel()
|
|
|
|
|
2016-06-28 17:35:48 +00:00
|
|
|
handler, ok := serviceHandlers[service]
|
|
|
|
if !ok {
|
|
|
|
http.Error(res, "Service not found: "+service, http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-06-28 22:51:51 +00:00
|
|
|
title, text, color, err := handler.Handle(ctx, params)
|
2016-06-28 17:35:48 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(res, "Error while executing service: "+err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-07 13:24:33 +00:00
|
|
|
renderBadgeToResponse(al, r, title, text, color)
|
2016-06-28 17:35:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func generateBadge(res http.ResponseWriter, r *http.Request) {
|
|
|
|
title := r.URL.Query().Get("title")
|
|
|
|
text := r.URL.Query().Get("text")
|
|
|
|
color := r.URL.Query().Get("color")
|
|
|
|
|
|
|
|
if title == "" || text == "" {
|
|
|
|
http.Error(res, "You must specify parameters 'title' and 'text'.", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if color == "" {
|
|
|
|
color = defaultColor
|
|
|
|
}
|
|
|
|
|
2016-06-28 22:52:49 +00:00
|
|
|
http.Redirect(res, r, fmt.Sprintf("/static/%s/%s/%s",
|
|
|
|
url.QueryEscape(title),
|
|
|
|
url.QueryEscape(text),
|
|
|
|
url.QueryEscape(color),
|
|
|
|
), http.StatusMovedPermanently)
|
2016-06-28 17:35:48 +00:00
|
|
|
}
|
|
|
|
|
2016-06-28 19:15:41 +00:00
|
|
|
func renderBadgeToResponse(res http.ResponseWriter, r *http.Request, title, text, color string) {
|
2016-07-06 21:56:48 +00:00
|
|
|
cacheKey := fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprintf("%s::::%s::::%s", title, text, color))))
|
|
|
|
storedTag, _ := cacheStore.Get("eTag", cacheKey)
|
2016-06-28 17:35:48 +00:00
|
|
|
|
2016-06-28 19:15:41 +00:00
|
|
|
res.Header().Add("Cache-Control", "no-cache")
|
2016-06-28 17:35:48 +00:00
|
|
|
|
2016-07-06 21:56:48 +00:00
|
|
|
if storedTag != "" && r.Header.Get("If-None-Match") == storedTag {
|
|
|
|
res.Header().Add("ETag", storedTag)
|
2016-06-28 19:15:41 +00:00
|
|
|
res.WriteHeader(http.StatusNotModified)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-07-06 21:56:48 +00:00
|
|
|
badge, eTag := createBadge(title, text, color)
|
|
|
|
cacheStore.Set("eTag", cacheKey, eTag, time.Hour)
|
|
|
|
|
|
|
|
res.Header().Add("ETag", eTag)
|
2016-06-28 19:15:41 +00:00
|
|
|
res.Header().Add("Content-Type", "image/svg+xml")
|
|
|
|
|
2016-06-28 17:35:48 +00:00
|
|
|
m := minify.New()
|
|
|
|
m.AddFunc("image/svg+xml", svg.Minify)
|
|
|
|
|
|
|
|
badge, _ = m.Bytes("image/svg+xml", badge)
|
|
|
|
|
|
|
|
res.Write(badge)
|
|
|
|
}
|
|
|
|
|
|
|
|
func createBadge(title, text, color string) ([]byte, string) {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
|
|
titleW, _ := calculateTextWidth(title)
|
|
|
|
textW, _ := calculateTextWidth(text)
|
|
|
|
|
|
|
|
width := titleW + textW + 4*xSpacing
|
|
|
|
|
|
|
|
t, _ := Asset("assets/badgeTemplate.tpl")
|
|
|
|
tpl, _ := template.New("svg").Parse(string(t))
|
|
|
|
|
2016-07-05 14:41:48 +00:00
|
|
|
if c, ok := colorList[color]; ok {
|
|
|
|
color = c
|
|
|
|
}
|
|
|
|
|
2016-06-28 17:35:48 +00:00
|
|
|
tpl.Execute(&buf, map[string]interface{}{
|
|
|
|
"Width": width,
|
|
|
|
"TitleWidth": titleW + 2*xSpacing,
|
|
|
|
"Title": title,
|
|
|
|
"Text": text,
|
|
|
|
"TitleAnchor": titleW/2 + xSpacing,
|
|
|
|
"TextAnchor": titleW + textW/2 + 3*xSpacing,
|
|
|
|
"Color": color,
|
|
|
|
})
|
|
|
|
|
|
|
|
return buf.Bytes(), generateETag(buf.Bytes())
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateETag(in []byte) string {
|
|
|
|
return fmt.Sprintf("%x", sha1.Sum(in))
|
|
|
|
}
|
2016-06-28 21:56:22 +00:00
|
|
|
|
|
|
|
func handleDemoPage(res http.ResponseWriter, r *http.Request) {
|
|
|
|
t, _ := Asset("assets/demoPage.tpl.html")
|
|
|
|
tpl, _ := template.New("demoPage").Parse(string(t))
|
|
|
|
|
|
|
|
examples := serviceHandlerDocumentationList{}
|
|
|
|
|
|
|
|
for register, handler := range serviceHandlers {
|
2016-06-29 11:09:08 +00:00
|
|
|
tmps := handler.GetDocumentation()
|
|
|
|
for _, tmp := range tmps {
|
|
|
|
tmp.Register = register
|
|
|
|
examples = append(examples, tmp)
|
|
|
|
}
|
2016-06-28 21:56:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sort.Sort(examples)
|
|
|
|
|
|
|
|
tpl.Execute(res, map[string]interface{}{
|
|
|
|
"Examples": examples,
|
2016-06-28 22:22:48 +00:00
|
|
|
"Version": version,
|
2016-06-28 21:56:22 +00:00
|
|
|
})
|
|
|
|
}
|