Domain and favicon compilable with inkscape to convert svg to png
ci/woodpecker/push/build Pipeline was successful
Details
ci/woodpecker/push/build Pipeline was successful
Details
This commit is contained in:
parent
3362df508e
commit
5d7f23fb29
|
@ -0,0 +1,20 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="GoUnhandledErrorResult" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<methods>
|
||||
<method importPath="hash" receiver="Hash" name="Write" />
|
||||
<method importPath="strings" receiver="*Builder" name="Write" />
|
||||
<method importPath="strings" receiver="*Builder" name="WriteByte" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteRune" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="Write" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteString" />
|
||||
<method importPath="strings" receiver="*Builder" name="WriteString" />
|
||||
<method importPath="bytes" receiver="*Buffer" name="WriteByte" />
|
||||
<method importPath="strings" receiver="*Builder" name="WriteRune" />
|
||||
<method importPath="math/rand" receiver="*Rand" name="Read" />
|
||||
<method importPath="github.com/galihrivanto/go-inkscape" receiver="*Proxy" name="Close" />
|
||||
</methods>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
2
Makefile
2
Makefile
|
@ -26,7 +26,7 @@ test:
|
|||
$(CC) test ./... -tags TEST
|
||||
|
||||
dev:
|
||||
docker compose -f docker-compose.development.yml build --no-cache
|
||||
docker compose -f docker-compose.development.yml build
|
||||
docker compose -f docker-compose.development.yml up
|
||||
|
||||
dev-down:
|
||||
|
|
|
@ -6,4 +6,4 @@ COPY ../.. .
|
|||
RUN go get -d -v ./...
|
||||
RUN ./scripts/build.sh "/bin/azalea" "./cmd/azalea/"
|
||||
|
||||
CMD ["azalea"]
|
||||
CMD ["/bin/azalea"]
|
||||
|
|
|
@ -12,6 +12,7 @@ type AzaleaConfig struct {
|
|||
RateLimit uint64 `yaml:"rateLimit"`
|
||||
Logs LogConfig `yaml:"logs"`
|
||||
Code418 string `yaml:"418"`
|
||||
Inkscape string `yaml:"inkscape"`
|
||||
}
|
||||
|
||||
type ListenConfig struct {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.19
|
||||
FROM golang:1.19 AS build
|
||||
|
||||
WORKDIR /go/src/app
|
||||
COPY ../.. .
|
||||
|
@ -6,4 +6,10 @@ COPY ../.. .
|
|||
RUN go get -d -v ./...
|
||||
RUN ./scripts/build.sh "/bin/azalea" "./cmd/azalea/" "DEBUG"
|
||||
|
||||
CMD ["azalea"]
|
||||
FROM debian:latest
|
||||
RUN apt-get update && apt-get install -y inkscape \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /root
|
||||
COPY --from=build /bin/azalea .
|
||||
|
||||
CMD ["./azalea"]
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package domain_checker
|
||||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/web"
|
||||
"code.mrmelon54.com/melon/summer/pkg/utils"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func New(db *xorm.Engine) *DomainChecker {
|
||||
d := &DomainChecker{
|
||||
db: db,
|
||||
domainSync: &sync.RWMutex{},
|
||||
domainMap: make(map[string]struct{}),
|
||||
}
|
||||
d.Compile()
|
||||
return d
|
||||
}
|
||||
|
||||
type DomainChecker struct {
|
||||
db *xorm.Engine
|
||||
domainSync *sync.RWMutex
|
||||
domainMap map[string]struct{}
|
||||
}
|
||||
|
||||
func (d *DomainChecker) Compile() {
|
||||
go func() {
|
||||
domainMap := make(map[string]struct{})
|
||||
err := d.internalCompile(domainMap)
|
||||
if err != nil {
|
||||
// log compile errors
|
||||
log.Printf("[DomainChecker] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
// lock while replacing the map
|
||||
d.domainSync.Lock()
|
||||
d.domainMap = domainMap
|
||||
d.domainSync.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (d *DomainChecker) internalCompile(domainMap map[string]struct{}) error {
|
||||
log.Println("[DomainChecker] Pulling from database")
|
||||
var domains []web.Domain
|
||||
err := d.db.Find(&domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, i := range domains {
|
||||
domainMap[i.Domain] = struct{}{}
|
||||
}
|
||||
|
||||
// well I guess we are done
|
||||
log.Printf("[DomainChecker] Finished compiling, %d domains will be available shortly\n", len(domains))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DomainChecker) IsValid(host string) bool {
|
||||
domain, ok := utils.GetDomainWithoutPort(host)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
d.domainSync.RLock()
|
||||
defer d.domainSync.RUnlock()
|
||||
n := strings.Split(domain, ".")
|
||||
for i := 0; i < len(n); i++ {
|
||||
if _, ok := d.domainMap[strings.Join(n[i:], ".")]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package domain_checker
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainChecker_IsValid(t *testing.T) {
|
||||
d := &DomainChecker{
|
||||
domainSync: &sync.RWMutex{},
|
||||
domainMap: map[string]struct{}{"summer.test": {}},
|
||||
}
|
||||
assert.True(t, d.IsValid("this.is.a.test.summer.test"))
|
||||
assert.True(t, d.IsValid("summer.test"))
|
||||
assert.False(t, d.IsValid("test"))
|
||||
assert.False(t, d.IsValid("not-summer.test"))
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/web"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mrmelon54/png2ico"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var ErrFaviconNotFound = errors.New("favicon not found")
|
||||
|
||||
func New(db *xorm.Engine, inkscapeCmd string) *Favicons {
|
||||
f := &Favicons{
|
||||
db: db,
|
||||
cmd: inkscapeCmd,
|
||||
cLock: &sync.RWMutex{},
|
||||
faviconMap: make(map[string]*FaviconList),
|
||||
}
|
||||
f.Compile()
|
||||
return f
|
||||
}
|
||||
|
||||
type Favicons struct {
|
||||
db *xorm.Engine
|
||||
cmd string
|
||||
cLock *sync.RWMutex
|
||||
faviconMap map[string]*FaviconList
|
||||
}
|
||||
|
||||
func (f *Favicons) Compile() {
|
||||
go func() {
|
||||
favicons := make(map[string]*FaviconList)
|
||||
err := f.internalCompile(favicons)
|
||||
if err != nil {
|
||||
// log compile errors
|
||||
log.Printf("[Favicons] Compile failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
// lock while replacing the map
|
||||
f.cLock.Lock()
|
||||
f.faviconMap = favicons
|
||||
f.cLock.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
func (f *Favicons) GetIcons(host string) (*FaviconList, bool) {
|
||||
if a, ok := f.faviconMap[host]; ok {
|
||||
return a, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (f *Favicons) internalCompile(faviconMap map[string]*FaviconList) error {
|
||||
var faviconList []web.HttpFavicon
|
||||
err := f.db.Find(&faviconList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
for _, i := range faviconList {
|
||||
l := &FaviconList{
|
||||
Ico: CreateFaviconImage(i.Ico),
|
||||
Png: CreateFaviconImage(i.Png),
|
||||
Svg: CreateFaviconImage(i.Svg),
|
||||
}
|
||||
faviconMap[i.Host] = l
|
||||
g.Go(func() error {
|
||||
return l.PreProcess(f.convertSvgToPng)
|
||||
})
|
||||
}
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (f *Favicons) convertSvgToPng(in []byte) ([]byte, error) {
|
||||
return svg2png(f.cmd, in)
|
||||
}
|
||||
|
||||
type FaviconList struct {
|
||||
Ico *FaviconImage // can be generated from png with wrapper
|
||||
Png *FaviconImage // can be generated from svg with inkscape
|
||||
Svg *FaviconImage
|
||||
}
|
||||
|
||||
func (l *FaviconList) ProduceIco() ([]byte, error) {
|
||||
if l.Ico == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Ico.Raw, nil
|
||||
}
|
||||
|
||||
func (l *FaviconList) ProducePng() ([]byte, error) {
|
||||
if l.Png == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Png.Raw, nil
|
||||
}
|
||||
|
||||
func (l *FaviconList) ProduceSvg() ([]byte, error) {
|
||||
if l.Svg == nil {
|
||||
return nil, ErrFaviconNotFound
|
||||
}
|
||||
return l.Svg.Raw, nil
|
||||
}
|
||||
|
||||
func (l *FaviconList) PreProcess(convert func(in []byte) ([]byte, error)) error {
|
||||
var err error
|
||||
|
||||
// SVG
|
||||
if l.Svg != nil {
|
||||
// download SVG
|
||||
l.Svg.Raw, err = getFaviconViaRequest(l.Svg.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch SVG icon: %w", err)
|
||||
}
|
||||
l.Svg.Hash = hex.EncodeToString(sha256.New().Sum(l.Svg.Raw))
|
||||
}
|
||||
|
||||
// PNG
|
||||
if l.Png != nil {
|
||||
// download PNG
|
||||
l.Png.Raw, err = getFaviconViaRequest(l.Png.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch PNG icon: %w", err)
|
||||
}
|
||||
} else if l.Svg != nil {
|
||||
// generate PNG from SVG
|
||||
l.Png = &FaviconImage{}
|
||||
l.Png.Raw, err = convert(l.Svg.Raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to generate PNG icon: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ICO
|
||||
if l.Ico != nil {
|
||||
// download ICO
|
||||
l.Ico.Raw, err = getFaviconViaRequest(l.Ico.Url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to fetch ICO icon: %w", err)
|
||||
}
|
||||
} else if l.Png != nil {
|
||||
// generate ICO from PNG
|
||||
l.Ico = &FaviconImage{}
|
||||
decode, err := png.Decode(bytes.NewReader(l.Png.Raw))
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to decode PNG icon: %w", err)
|
||||
}
|
||||
b := decode.Bounds()
|
||||
l.Ico.Raw, err = png2ico.ConvertPngToIco(l.Png.Raw, b.Dx(), b.Dy())
|
||||
if err != nil {
|
||||
return fmt.Errorf("[Favicons] Failed to generate ICO icon: %w", err)
|
||||
}
|
||||
}
|
||||
l.Svg.Hash = genSha256(l.Svg.Raw)
|
||||
l.Png.Hash = genSha256(l.Png.Raw)
|
||||
l.Ico.Hash = genSha256(l.Ico.Raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFaviconViaRequest(url string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to send request '%s': %w", url, err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to do request '%s': %w", url, err)
|
||||
}
|
||||
rawBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[Favicons] Failed to read response '%s': %w", url, err)
|
||||
}
|
||||
return rawBody, nil
|
||||
}
|
||||
|
||||
func genSha256(in []byte) string {
|
||||
h := sha256.New()
|
||||
_, err := h.Write(in)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
type FaviconImage struct {
|
||||
Url string
|
||||
Hash string
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
func CreateFaviconImage(url string) *FaviconImage {
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
return &FaviconImage{Url: url}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package favicons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func svg2png(inkscapeCmd string, in []byte) (out []byte, err error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
cmd := exec.Command(inkscapeCmd, "--export-type", "png", "--export-filename", "-", "--export-background-opacity", "0", "--pipe")
|
||||
cmd.Stdin = bytes.NewBuffer(in)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if e := cmd.Run(); e != nil {
|
||||
err = fmt.Errorf("%s\nSTDERR:\n%s", e.Error(), stderr.String())
|
||||
return
|
||||
}
|
||||
|
||||
if stdout.Len() == 0 {
|
||||
err = fmt.Errorf("got no data from inkscape")
|
||||
return
|
||||
}
|
||||
|
||||
out = stdout.Bytes()
|
||||
|
||||
return
|
||||
}
|
|
@ -2,9 +2,12 @@ package main
|
|||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/config"
|
||||
domainChecker "code.mrmelon54.com/melon/summer/cmd/azalea/domain-checker"
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/favicons"
|
||||
quickDb "code.mrmelon54.com/melon/summer/cmd/azalea/quick-db"
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/routing"
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/servers"
|
||||
"code.mrmelon54.com/melon/summer/pkg/api"
|
||||
"code.mrmelon54.com/melon/summer/pkg/cli"
|
||||
"code.mrmelon54.com/melon/summer/pkg/proxy"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/certificate"
|
||||
|
@ -33,6 +36,7 @@ var usedTables = []any{
|
|||
&web.HttpRouteHeader{},
|
||||
&web.HttpServiceHeader{},
|
||||
&web.HttpDefaultHost{},
|
||||
&web.HttpFavicon{},
|
||||
|
||||
// renewal
|
||||
&renewal.AcmeContent{},
|
||||
|
@ -85,11 +89,14 @@ func (azalea *Azalea) Init(runner *cli.Runner[config.AzaleaConfig]) {
|
|||
|
||||
azalea.reverseProxy = proxy.CreateHybridReverseProxy()
|
||||
tableRouter, err := routing.New(runner.Database, azalea.reverseProxy, azalea.accessLog, fourEighteenErrorFunc)
|
||||
domainCheck := domainChecker.New(runner.Database)
|
||||
faviconMap := favicons.New(runner.Database, azalea.conf.Inkscape)
|
||||
compileTargets := api.MultiCompilable{tableRouter, domainCheck}
|
||||
utils.Check("[Azalea.Init()] Failed to load table router:", err)
|
||||
|
||||
azalea.httpServer = servers.NewHttpServer(azalea.conf.Listen.Http, addrPort, azalea.runner.Database)
|
||||
azalea.httpsServer = servers.NewHttpsServer(azalea.conf.Listen.Https, azalea.runner.Database, tableRouter, azalea.conf, fourEighteenErrorFunc)
|
||||
azalea.apiServer = servers.NewApiServer(azalea.conf.Listen.Api, azalea.runner.Database, azalea.verify, tableRouter)
|
||||
azalea.httpServer = servers.NewHttpServer(azalea.conf.Listen.Http, addrPort, azalea.runner.Database, domainCheck)
|
||||
azalea.httpsServer = servers.NewHttpsServer(azalea.conf.Listen.Https, azalea.runner.Database, tableRouter, domainCheck, faviconMap, azalea.conf, fourEighteenErrorFunc)
|
||||
azalea.apiServer = servers.NewApiServer(azalea.conf.Listen.Api, azalea.runner.Database, azalea.verify, compileTargets)
|
||||
}
|
||||
|
||||
func (azalea *Azalea) Destroy() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package servers
|
||||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/routing"
|
||||
"code.mrmelon54.com/melon/summer/pkg/api"
|
||||
"code.mrmelon54.com/melon/summer/pkg/api/crud"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/web"
|
||||
"code.mrmelon54.com/melon/summer/pkg/utils"
|
||||
|
@ -13,13 +13,13 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func NewApiServer(listen string, db *xorm.Engine, verify mjwt.Provider, tableRouter *routing.TableRouter) *http.Server {
|
||||
func NewApiServer(listen string, db *xorm.Engine, verify mjwt.Provider, compileTarget api.MultiCompilable) *http.Server {
|
||||
router := mux.NewRouter()
|
||||
crud.NewCrudHandler[web.HttpService](router, verify, crud.NewSinglePermProvider[web.HttpService]("manage:http-service", crud.NewDatabaseProvider[web.HttpService](db, "id")), "/http-services", "/http-service/{id}")
|
||||
crud.NewCrudHandler[web.HttpRedirect](router, verify, crud.NewSinglePermProvider[web.HttpRedirect]("manage:http-redirect", crud.NewDatabaseProvider[web.HttpRedirect](db, "id")), "/http-redirects", "/http-redirect/{id}")
|
||||
crud.NewCrudHandler[web.ApiRoute](router, verify, crud.NewSinglePermProvider[web.ApiRoute]("manage:api-route", crud.NewDatabaseProvider[web.ApiRoute](db, "id")), "/api-routes", "/api-route/{id}")
|
||||
router.HandleFunc("/compile", func(rw http.ResponseWriter, req *http.Request) {
|
||||
tableRouter.Compile()
|
||||
compileTarget.Compile()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}).Methods(http.MethodPost)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package servers
|
||||
|
||||
import (
|
||||
domainChecker "code.mrmelon54.com/melon/summer/cmd/azalea/domain-checker"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/renewal"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/web"
|
||||
"code.mrmelon54.com/melon/summer/pkg/utils"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -12,12 +12,12 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func NewHttpServer(listen string, httpsPort uint16, db *xorm.Engine) *http.Server {
|
||||
func NewHttpServer(listen string, httpsPort uint16, db *xorm.Engine, domainCheck *domainChecker.DomainChecker) *http.Server {
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/.well-known/acme-challenge/{token}", func(rw http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
if hostname, ok := utils.GetDomainWithoutPort(req.Host); ok {
|
||||
if !web.CheckDomainInDb(db, hostname) {
|
||||
if !domainCheck.IsValid(req.Host) {
|
||||
utils.RequestKiller(rw)
|
||||
return
|
||||
}
|
||||
|
@ -41,10 +41,6 @@ func NewHttpServer(listen string, httpsPort uint16, db *xorm.Engine) *http.Serve
|
|||
})
|
||||
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if h, ok := utils.GetDomainWithoutPort(req.Host); ok {
|
||||
if !web.CheckDomainInDb(db, h) {
|
||||
utils.RequestKiller(rw)
|
||||
return
|
||||
}
|
||||
if httpsPort != 443 {
|
||||
req.URL.Host = fmt.Sprintf("%s:%d", h, httpsPort)
|
||||
} else {
|
||||
|
|
|
@ -2,9 +2,10 @@ package servers
|
|||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/config"
|
||||
domainChecker "code.mrmelon54.com/melon/summer/cmd/azalea/domain-checker"
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/favicons"
|
||||
"code.mrmelon54.com/melon/summer/cmd/azalea/routing"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/certificate"
|
||||
"code.mrmelon54.com/melon/summer/pkg/tables/web"
|
||||
"code.mrmelon54.com/melon/summer/pkg/utils"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
@ -18,9 +19,9 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRouter, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) *http.Server {
|
||||
func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) *http.Server {
|
||||
router := mux.NewRouter()
|
||||
setupHttpsRouter(router, db, tableRouter, conf, fourEighteenError)
|
||||
setupHttpsRouter(router, tableRouter, domainCheck, faviconMap, conf, fourEighteenError)
|
||||
|
||||
certCache := utils.NewCertCache()
|
||||
|
||||
|
@ -29,7 +30,7 @@ func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRo
|
|||
Handler: router,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if !web.CheckDomainInDb(db, info.ServerName) {
|
||||
if !domainCheck.IsValid(info.ServerName) {
|
||||
return nil, fmt.Errorf("invalid hostname used: '%s'", info.ServerName)
|
||||
}
|
||||
|
||||
|
@ -62,12 +63,18 @@ func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRo
|
|||
return s
|
||||
}
|
||||
|
||||
func setupHttpsRouter(router *mux.Router, db *xorm.Engine, tableRouter *routing.TableRouter, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) {
|
||||
func setupHttpsRouter(router *mux.Router, tableRouter *routing.TableRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) {
|
||||
faviconColor := favicon.NewColor()
|
||||
|
||||
router.Use(setupRateLimiter(conf.RateLimit))
|
||||
router.HandleFunc("/favicon.{ext:(?:ico|png|svg)}", utils.CachedPage(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if !web.CheckDomainInDb(db, req.Host) {
|
||||
if !domainCheck.IsValid(req.Host) {
|
||||
fourEighteenError(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
domain, ok := utils.GetDomainWithoutPort(req.Host)
|
||||
if !ok {
|
||||
fourEighteenError(rw, req)
|
||||
return
|
||||
}
|
||||
|
@ -76,7 +83,12 @@ func setupHttpsRouter(router *mux.Router, db *xorm.Engine, tableRouter *routing.
|
|||
ext := vars["ext"]
|
||||
h := rw.Header()
|
||||
|
||||
favSvg := favicon.NewSvg(req.Host, faviconColor)
|
||||
var favSvg produceImg
|
||||
if faviconList, ok := faviconMap.GetIcons(domain); ok {
|
||||
favSvg = faviconList
|
||||
} else {
|
||||
favSvg = favicon.NewSvg(domain, faviconColor)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
var err error
|
||||
|
@ -102,7 +114,7 @@ func setupHttpsRouter(router *mux.Router, db *xorm.Engine, tableRouter *routing.
|
|||
_, _ = rw.Write(b)
|
||||
}))
|
||||
router.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
if !web.CheckDomainInDb(db, req.Host) {
|
||||
if !domainCheck.IsValid(req.Host) {
|
||||
fourEighteenError(rw, req)
|
||||
return
|
||||
}
|
||||
|
@ -125,3 +137,9 @@ func setupRateLimiter(rateLimit uint64) mux.MiddlewareFunc {
|
|||
}
|
||||
return middleware.Handle
|
||||
}
|
||||
|
||||
type produceImg interface {
|
||||
ProduceIco() ([]byte, error)
|
||||
ProducePng() ([]byte, error)
|
||||
ProduceSvg() ([]byte, error)
|
||||
}
|
||||
|
|
|
@ -20,5 +20,9 @@ func NewApiServer(listen string, db *xorm.Engine, renewal *renewal.Renewal, veri
|
|||
handleGetPublic(router, db, "/domain-certificate/{domain}")
|
||||
crud.NewCrudHandler[CertDbItem](router, verify, crud.NewSinglePermProvider[CertDbItem]("manage:certificate", NewCertDbProvider(db, renewal)), "/certificates", "/certificate/{id}")
|
||||
crud.NewCrudHandler[CertPrivate](router, verify, crud.NewSinglePermProvider[CertPrivate]("manage:certificate:admin", certPrivateProvider{db: db}), "/certificate-pairs", "/certificate-pair/{domain}")
|
||||
router.HandleFunc("/renew", func(rw http.ResponseWriter, req *http.Request) {
|
||||
renewal.Renew()
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
return api.RunApiServer(listen, router)
|
||||
}
|
||||
|
|
|
@ -10,3 +10,4 @@ rateLimit: 300
|
|||
logs:
|
||||
accessLogDir: logs
|
||||
418: '418.html'
|
||||
inkscape: 'inkscape'
|
||||
|
|
|
@ -8,3 +8,4 @@ auth:
|
|||
apiDomain: api.summer.test
|
||||
rateLimit: 300
|
||||
418: '/etc/melon-summer/418.html'
|
||||
inkscape: 'inkscape'
|
||||
|
|
|
@ -40,7 +40,6 @@ services:
|
|||
- "8443:443"
|
||||
links:
|
||||
- database
|
||||
- cli
|
||||
depends_on:
|
||||
- database
|
||||
buttercup:
|
||||
|
@ -55,7 +54,6 @@ services:
|
|||
links:
|
||||
- database
|
||||
- pebble
|
||||
- cli
|
||||
depends_on:
|
||||
- database
|
||||
- pebble
|
||||
|
@ -70,7 +68,6 @@ services:
|
|||
- "./config/marigold.config.yml:/etc/melon-summer/marigold.config.yml:ro"
|
||||
links:
|
||||
- database
|
||||
- cli
|
||||
depends_on:
|
||||
- database
|
||||
rose:
|
||||
|
@ -89,10 +86,5 @@ services:
|
|||
- "4321:4321/udp"
|
||||
links:
|
||||
- database
|
||||
- cli
|
||||
depends_on:
|
||||
- database
|
||||
cli:
|
||||
restart: on-failure
|
||||
image: ubuntu
|
||||
entrypoint: sleep infinity
|
||||
|
|
6
go.mod
6
go.mod
|
@ -12,11 +12,14 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/kjk/common v0.0.0-20220705191345-4e210bd3860d
|
||||
github.com/mattn/go-sqlite3 v1.14.9
|
||||
github.com/mrmelon54/favicon v1.0.0
|
||||
github.com/mrmelon54/mjwt v0.0.1
|
||||
github.com/mrmelon54/png2ico v1.0.0
|
||||
github.com/mskrha/svg2png v0.0.0-20220111070911-b5983936965c
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sec51/twofactor v1.0.0
|
||||
github.com/sethvargo/go-limiter v0.7.2
|
||||
|
@ -24,6 +27,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||
golang.org/x/exp v0.0.0-20220218215828-6cf2b201936e
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
xorm.io/xorm v1.3.1
|
||||
)
|
||||
|
@ -45,12 +49,10 @@ require (
|
|||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mrmelon54/png2ico v1.0.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sec51/convert v1.0.2 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -398,6 +398,8 @@ github.com/mrmelon54/mjwt v0.0.1 h1:XgyWviTmgsbMiKXjxo+Jp/QSf7FF7/omkvrUag8/P5U=
|
|||
github.com/mrmelon54/mjwt v0.0.1/go.mod h1:M+kZ6t9EArEQ2/CGjfgyNhAo542ot+S7gw5uJCK11Ms=
|
||||
github.com/mrmelon54/png2ico v1.0.0 h1:YE20i0xao8rkuYaCq3Xj2hUkVkJ6xp412aGDMrGqufA=
|
||||
github.com/mrmelon54/png2ico v1.0.0/go.mod h1:vp8Be9y5cz102ANon+BnsIzTUdet3VQRvOuWJTH9h0M=
|
||||
github.com/mskrha/svg2png v0.0.0-20220111070911-b5983936965c h1:WJZ9Z0iwgh4GO0HXjXWtZn+v8UqcLlVu2tqWaqbEACk=
|
||||
github.com/mskrha/svg2png v0.0.0-20220111070911-b5983936965c/go.mod h1:KFdfdIgpr48ODxdkxKvpcYwuyLpQ6rfkAsFB2UQ6jD4=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package api
|
||||
|
||||
type Compilable interface {
|
||||
Compile()
|
||||
}
|
||||
|
||||
type MultiCompilable []Compilable
|
||||
|
||||
func (m MultiCompilable) Compile() {
|
||||
for _, i := range m {
|
||||
i.Compile()
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ import (
|
|||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var ErrAlreadyRenewing = errors.New("renewal: already renewing")
|
||||
|
||||
type Renewal struct {
|
||||
db *xorm.Engine
|
||||
httpProvider *CustomHTTPProvider
|
||||
|
@ -30,6 +32,7 @@ type Renewal struct {
|
|||
certDone chan struct{}
|
||||
caAddr string
|
||||
caCert []byte
|
||||
renewLock *sync.Mutex
|
||||
}
|
||||
|
||||
type MyAccount struct {
|
||||
|
@ -51,7 +54,7 @@ func (u *MyAccount) GetPrivateKey() crypto.PrivateKey {
|
|||
}
|
||||
|
||||
func NewRenewalService(wg *sync.WaitGroup, db *xorm.Engine, httpProvider *CustomHTTPProvider, config Config) *Renewal {
|
||||
r := &Renewal{db: db, httpProvider: httpProvider}
|
||||
r := &Renewal{db: db, httpProvider: httpProvider, renewLock: &sync.Mutex{}}
|
||||
r.load(wg, config)
|
||||
return r
|
||||
}
|
||||
|
@ -107,7 +110,10 @@ func (r *Renewal) renewalRoutine(wg *sync.WaitGroup) {
|
|||
}()
|
||||
|
||||
log.Printf("[Renewal] Doing quick certificate check before starting...\n")
|
||||
r.renewalCheck()
|
||||
err := r.renewalCheck()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
log.Printf("[Renewal] Initial check complete, continually checking every 4 hours...\n")
|
||||
|
||||
for {
|
||||
|
@ -115,12 +121,17 @@ func (r *Renewal) renewalRoutine(wg *sync.WaitGroup) {
|
|||
case <-r.certDone:
|
||||
return
|
||||
case <-r.certTicker.C:
|
||||
r.renewalCheck()
|
||||
_ = r.renewalCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renewal) renewalCheck() {
|
||||
func (r *Renewal) renewalCheck() error {
|
||||
if !r.renewLock.TryLock() {
|
||||
return ErrAlreadyRenewing
|
||||
}
|
||||
r.renewLock.Unlock()
|
||||
|
||||
var certs []certificate2.CertificateMetaDomainJoiner
|
||||
//goland:noinspection SqlDialectInspection,SqlNoDataSourceInspection
|
||||
err := r.db.SQL(`
|
||||
|
@ -130,13 +141,15 @@ select * from certificate
|
|||
`).Find(&certs)
|
||||
if err != nil {
|
||||
log.Printf("[Renewal::renewalCheck()] Failed to get certificates for a renewal check: %s\n", err)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, i := range certs {
|
||||
log.Printf("[Renewal] Cert '%d' runs out in 30 days\n", i.Id)
|
||||
r.RequestUpdate(i)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renewal) RequestUpdate(c certificate2.CertificateMetaDomainJoiner) {
|
||||
|
@ -315,3 +328,7 @@ func (r *Renewal) setupForRequest(c *certificate2.CertificateMetaDomainJoiner) (
|
|||
myUser.Registration = qReg
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (r *Renewal) Renew() {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"code.mrmelon54.com/melon/summer/pkg/utils"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
Id uint64 `xorm:"pk autoincr"`
|
||||
Domain string
|
||||
}
|
||||
|
||||
func CheckDomainInDb(db *xorm.Engine, domain string) bool {
|
||||
if nDomain, ok := utils.GetDomainWithoutPort(domain); ok {
|
||||
if bDomain, ok := utils.GetTopFqdn(nDomain); ok {
|
||||
a, err := db.Where("domain = ?", bDomain).Count(&Domain{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return a >= 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package web
|
||||
|
||||
type HttpFavicon struct {
|
||||
Id uint64 `json:"id" xorm:"pk autoincr"`
|
||||
Host string `json:"host"`
|
||||
Ico string `json:"ico"` // auto convert from png if present
|
||||
Png string `json:"png"` // auto convert from svg if present
|
||||
Svg string `json:"svg"` // can't be auto converted
|
||||
}
|
Reference in New Issue