From abfc67d10f1da138b06d021d0b151c8963e63952 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 17 May 2024 06:45:45 -0500 Subject: [PATCH 01/17] Expirementing with building everthing from scratch. 1st steps --- email.go | 108 -------- go.mod | 31 +-- go.sum | 119 --------- internal/darkstorm_backend/README.md | 86 ++++++ internal/darkstorm_backend/app.go | 3 + internal/darkstorm_backend/darkstorm.go | 7 + internal/darkstormtech/app.go | 340 ------------------------ internal/flexmls/app.go | 14 - linker.go | 215 --------------- main.go | 44 --- stupid.go | 74 ------ web.go | 88 ------ 12 files changed, 97 insertions(+), 1032 deletions(-) delete mode 100644 email.go create mode 100644 internal/darkstorm_backend/README.md create mode 100644 internal/darkstorm_backend/app.go create mode 100644 internal/darkstorm_backend/darkstorm.go delete mode 100644 internal/darkstormtech/app.go delete mode 100644 internal/flexmls/app.go delete mode 100644 linker.go delete mode 100644 stupid.go delete mode 100644 web.go diff --git a/email.go b/email.go deleted file mode 100644 index a772456..0000000 --- a/email.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "crypto/tls" - "flag" - "io" - "log" - "time" - - smtp "github.com/emersion/go-smtp" -) - -func startSMTPServer() { - keyPath := flag.Arg(1) - if keyPath == "" { - log.Println("No argument given for key files. smtp signing off...") - quitChan <- "smtp arg" - return - } - tlsCert, err := tls.LoadX509KeyPair(keyPath+"/fullchain.pem", keyPath+"/key.pem") - if err != nil { - log.Println("Error while loading tls certificate:", err) - quitChan <- "smtp err" - return - } - cfg := &tls.Config{Certificates: []tls.Certificate{tlsCert}} - serv := smtp.NewServer(&SMTPBackend{ - tlsConfig: cfg, - }) - serv.Network = "tcp" - serv.Addr = ":587" - serv.TLSConfig = cfg - serv.Domain = "darkstorm.tech" - serv.WriteTimeout = 10 * time.Second - serv.ReadTimeout = 10 * time.Second - serv.MaxMessageBytes = 1024 * 1024 - serv.AllowInsecureAuth = true - serv.MaxRecipients = 2 - err = serv.ListenAndServeTLS() - log.Println("Error while serving smtp:", err) - quitChan <- "smtp err" -} - -type SMTPBackend struct { - tlsConfig *tls.Config -} - -func (b *SMTPBackend) NewSession(c *smtp.Conn) (smtp.Session, error) { - return NewSession(b.tlsConfig, c.Server().Addr) -} - -type SMTPSession struct { - tlsConfig *tls.Config - client *smtp.Client - addr string -} - -func NewSession(tlsConfig *tls.Config, addr string) (*SMTPSession, error) { - client, err := smtp.DialTLS(addr, tlsConfig) - if err != nil { - return nil, err - } - return &SMTPSession{ - tlsConfig: tlsConfig, - client: client, - addr: addr, - }, nil -} - -func (s *SMTPSession) Reset() { - var err error - s.client, err = smtp.DialTLS(s.addr, s.tlsConfig) - if err != nil { - log.Println("Error while resetting smtp session:", err) - } -} - -func (s *SMTPSession) Logout() error { - return s.client.Quit() -} - -func (s *SMTPSession) AuthPlain(username, password string) error { - //TODO - return nil -} - -func (s *SMTPSession) Mail(from string, opts *smtp.MailOptions) error { - return s.client.Mail(from, opts) -} - -func (s *SMTPSession) Rcpt(to string, opts *smtp.RcptOptions) error { - return s.client.Rcpt(to, opts) -} - -func (s *SMTPSession) Data(r io.Reader) error { - wrt, err := s.client.Data() - if err != nil { - log.Println("Error while writing smtp data:", err) - return err - } - _, err = io.Copy(wrt, r) - if err != nil { - log.Println("Error while writing smtp data:", err) - return err - } - wrt.Close() - return nil -} diff --git a/go.mod b/go.mod index f0b17a9..e88cfaf 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,3 @@ module github.com/CalebQ42/darkstorm-server -go 1.22.2 - -require ( - github.com/1lann/udp-forward v0.0.0-20191015034046-6b774a53ea39 - github.com/CalebQ42/bbConvert v1.0.0 - github.com/CalebQ42/cdr-backend v0.1.1 - github.com/CalebQ42/stupid-backend/v2 v2.0.5 - github.com/CalebQ42/swassistant-backend v0.2.1 - go.mongodb.org/mongo-driver v1.15.0 -) - -require github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect - -require ( - github.com/emersion/go-smtp v0.21.1 - github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.17.8 // indirect - github.com/lithammer/shortuuid/v3 v3.0.7 // indirect - github.com/montanaflynn/stats v0.7.1 // indirect - github.com/pascaldekloe/jwt v1.12.0 // indirect - github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect - github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect -) +go 1.22.3 diff --git a/go.sum b/go.sum index 37331b0..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,119 +0,0 @@ -github.com/1lann/udp-forward v0.0.0-20191015034046-6b774a53ea39 h1:wAEqazqaAqb7wwijTl14KruBl7cvYytD3SkhXT9v5zs= -github.com/1lann/udp-forward v0.0.0-20191015034046-6b774a53ea39/go.mod h1:zK6NTEHRcxPf9N4gcm0WXvE8RpsJfg/a8hMSW6dAQ0c= -github.com/CalebQ42/bbConvert v1.0.0 h1:2WSAxVKhCCMReuU30r3ehLtL6m9aH8sY3wugp9yUdzg= -github.com/CalebQ42/bbConvert v1.0.0/go.mod h1:QJevnlhzUdL3EJB5Lgqoi7rdgtzt/UEamn/QGNoVgkM= -github.com/CalebQ42/cdr-backend v0.1.0 h1:9245aooAyLxAFO5gfXPgpqOHaYr6NAkMI75v2IlPOVA= -github.com/CalebQ42/cdr-backend v0.1.0/go.mod h1:N7A+ia+4GDsDMZ3gb5IRZ6CY07gdFfJECtR9csKh5nI= -github.com/CalebQ42/cdr-backend v0.1.1 h1:OwSVMODCPYMw3HpYTxqCAq9L1k0flLuPb5ICNv8qAn8= -github.com/CalebQ42/cdr-backend v0.1.1/go.mod h1:A0YjZk5xKAFXBNPdJ3HhsUgH37kY8Cxjt6isSU0o+Ok= -github.com/CalebQ42/stupid-backend/v2 v2.0.4 h1:ph75UDj5JevTiGyJbKgZsH3xyZZAGmXqfwSR6gLWoYQ= -github.com/CalebQ42/stupid-backend/v2 v2.0.4/go.mod h1:skBYIF77NzxYcqZ34V1eSD2/MIZCAGyyINWIkCBrpx8= -github.com/CalebQ42/stupid-backend/v2 v2.0.5 h1:l6lCzzAF0SCkGFLUiLMfWfBOP41uB/8Y3L50GuH2Npg= -github.com/CalebQ42/stupid-backend/v2 v2.0.5/go.mod h1:skBYIF77NzxYcqZ34V1eSD2/MIZCAGyyINWIkCBrpx8= -github.com/CalebQ42/swassistant-backend v0.2.0 h1:pXUG7+uHP5/lHaqqD6Hc64hOjfPzCGO8TkOpvtDB238= -github.com/CalebQ42/swassistant-backend v0.2.0/go.mod h1:m67UAzh552+puEc0LngTHzOL3b/Y2B19NzbaVdVeabI= -github.com/CalebQ42/swassistant-backend v0.2.1 h1:DqBx1pvPgMOE7LbzxvSoasH83FDgGLVv8JUhvA8CAZ0= -github.com/CalebQ42/swassistant-backend v0.2.1/go.mod h1:WY+3UvzBcTUoZMtYCVhNWfFW/Cx3lBActCJfk+EUD0s= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= -github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-smtp v0.20.2 h1:peX42Qnh5Q0q3vrAnRy43R/JwTnnv75AebxbkTL7Ia4= -github.com/emersion/go-smtp v0.20.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/emersion/go-smtp v0.21.1 h1:VQeZSZAKk8ueYii1yR5Zalmy7jI287eWDUqSaJ68vRM= -github.com/emersion/go-smtp v0.21.1/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= -github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= -github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= -github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ= -github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U= -github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= -github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= -github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo= -github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= -go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= -go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md new file mode 100644 index 0000000..595c91b --- /dev/null +++ b/internal/darkstorm_backend/README.md @@ -0,0 +1,86 @@ +# Darkstorm Backend + +This is a purposefully "simple" application backend made specifically for _my_ apps. It's purpose is to collect minimal (only what's absolutely necessary) amounts of data while still fulfilling all my needs. I've found that other, off the shelf options such as Firebase are a bit heavy on the data collection. Plus I like to make things :P. + +## Standard Header + +Any request might or might not need these values. These values can be authenticated via the `TODO` function. + +```json +{ + X-API-Key: "{API Key}", + Authorization: "Bearer {JWT Token}" +} +``` + +## Error Response + +If an error status code is returned then the body will be as follows. + +```json +{ + errorCode: "Error value for internal use", + errorMsg: "User error message", //This message is meant to be displayed to the user. May be empty. +} +``` + +## Users + +> TODO: Add the ability to create users and log-in through third-parties (such as Google). + +All requsests pertaining to users requires the `X-API-Key` header. + +### Create User + +> TODO: Email user to confirm. +> TODO: Screen username for offensive words and phrases. + +Request: + +```json +{ + username: "Username", + password: "Password", + email: "Email", +} +``` + +Return: + +```json +{ + username: "Username", + token: "JWT Token" +} +``` + +If returned status is 401, the errorCode will be one of the following: + +* username + * Username is already taken +* password + * Password does not follow rules +* email + * Email is already linked to an account +* disallowed + * Username contains words/phases that are not allowed + +### Login + +Request: + +```json +{ + username: "Username", + password: "Password", +} +``` + +Return: + +```json +{ + token: "JWT Token", + timeout: 0, +} +``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go new file mode 100644 index 0000000..a1e2aca --- /dev/null +++ b/internal/darkstorm_backend/app.go @@ -0,0 +1,3 @@ +package darkstorm + +type App interface{} \ No newline at end of file diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go new file mode 100644 index 0000000..8b49e57 --- /dev/null +++ b/internal/darkstorm_backend/darkstorm.go @@ -0,0 +1,7 @@ +package darkstorm + +import "net/http" + +type Backend struct { + http.ServeMux +} diff --git a/internal/darkstormtech/app.go b/internal/darkstormtech/app.go deleted file mode 100644 index 922291b..0000000 --- a/internal/darkstormtech/app.go +++ /dev/null @@ -1,340 +0,0 @@ -package darkstormtech - -import ( - "context" - "encoding/json" - "io" - "io/fs" - "log" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/CalebQ42/bbConvert" - "github.com/CalebQ42/stupid-backend/v2" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -type DarkstormTech struct { - stupid.UnKeyedApp - bb *bbConvert.HTMLConverter - DB *mongo.Database - filesFolder string -} - -func NewDarkstormTech(c *mongo.Client, filesFolder string) *DarkstormTech { - bb := &bbConvert.HTMLConverter{} - bb.ImplementDefaults() - return &DarkstormTech{ - bb: bb, - DB: c.Database("darkstormtech"), - filesFolder: filesFolder, - } -} - -func (d *DarkstormTech) AlternateName() string { - return "page" -} - -type pageOut struct { - Content string `json:"content"` - Title string `json:"title"` - Favicon string `json:"favicon"` -} - -func notFoundPage() pageOut { - return pageOut{ - Content: "404 Page Not Found 😥", - Title: "Darkstorm.Tech", - Favicon: "https://darkstorm.tech/favicon.png", - } -} - -func pageWith(content string, title string) pageOut { - if title == "" { - title = "Darkstorm.Tech" - } - return pageOut{ - Content: content, - Title: title, - Favicon: "https://darkstorm.tech/favicon.png", - } -} - -func (p pageOut) json() []byte { - out, _ := json.Marshal(p) - return out -} - -func (p *pageOut) addDefaults() { - if p.Title == "" { - p.Title = "Darkstorm.Tech" - } - if p.Favicon == "" { - p.Favicon = "https://darkstorm.tech/favicon.png" - } -} - -func (d *DarkstormTech) HandleReqest(req *stupid.Request) bool { - if req.Path[0] != "page" { - return false - } - if len(req.Path) == 1 { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - switch req.Path[1] { - case "blog": - return d.handleBlog(req) - case "files": - return d.handleFiles(req) - case "portfolio": - return d.handlePortfolio(req) - case "default": - b, err := d.getBlog(req) - if err == mongo.ErrNoDocuments { - req.Resp.Write(notFoundPage().json()) - req.Resp.WriteHeader(http.StatusNotFound) - return true - } else if err != nil { - log.Println("Error while getting blog:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - out := pageOut{ - Content: d.bb.Convert(b.Content), - } - (&out).addDefaults() - _, err = req.Resp.Write(out.json()) - if err != nil { - log.Println("Error while writing response:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - } - return true - } - res := d.DB.Collection("pages").FindOne(context.TODO(), bson.M{"_id": strings.Join(req.Path[1:], "/")}, options.FindOne().SetProjection(bson.M{"_id": 0})) - if res.Err() == mongo.ErrNoDocuments { - req.Resp.Write(notFoundPage().json()) - req.Resp.WriteHeader(http.StatusNotFound) - return true - } else if res.Err() != nil { - log.Println("Error while getting page:", res.Err()) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - var pag pageOut - err := res.Decode(&pag) - if err != nil { - log.Println("Error while decoding page:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - pag.Content = d.bb.Convert(pag.Content) - (&pag).addDefaults() - _, err = req.Resp.Write(pag.json()) - if err != nil { - log.Println("Error while writing response:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - } - return true -} - -type blog struct { - ID string `bson:"_id" json:"id"` - Title string `bson:"title" json:"title"` - Content string `bson:"content" json:"content"` -} - -func (d *DarkstormTech) getBlog(req *stupid.Request) (*blog, error) { - var res *mongo.SingleResult - if len(req.Path) == 2 { - res = d.DB.Collection("blog").FindOne(context.TODO(), bson.M{}, options.FindOne().SetSort(bson.M{"_id": -1})) - } else { - res = d.DB.Collection("blog").FindOne(context.TODO(), bson.M{"_id": req.Path[2]}) - } - if res.Err() != nil { - return nil, res.Err() - } - var b blog - err := res.Decode(&b) - if err != nil { - return nil, err - } - return &b, nil -} - -func (d *DarkstormTech) handleBlog(req *stupid.Request) bool { - if req.Method == http.MethodPost { - return d.addBlog(req) - } else if req.Method != http.MethodGet { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - b, err := d.getBlog(req) - if err == mongo.ErrNoDocuments { - req.Resp.Write(notFoundPage().json()) - req.Resp.WriteHeader(http.StatusNotFound) - return true - } else if err != nil { - log.Println("Error while getting blog:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - out := pageOut{ - Content: d.bb.Convert(b.Content), - Title: b.Title, - } - (&out).addDefaults() - _, err = req.Resp.Write(out.json()) - if err != nil { - log.Println("Error while writing response:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - } - return true -} - -func (d *DarkstormTech) addBlog(req *stupid.Request) bool { - if req.User == nil || req.User.Role != "admin" { - req.Resp.WriteHeader(http.StatusUnauthorized) - return true - } - if req.Body == nil { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - bod, err := io.ReadAll(req.Body) - req.Body.Close() - if err != nil { - log.Println("Error while reading body:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - if len(bod) == 0 { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - var b blog - err = json.Unmarshal(bod, &b) - if err != nil { - log.Println("Error while unmarshalling body:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - b.ID = strconv.Itoa(int(time.Now().Unix())) - _, err = d.DB.Collection("blog").InsertOne(context.TODO(), b) - if err != nil { - log.Println("Error while inserting blog:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - return true -} - -func (d *DarkstormTech) handleFiles(req *stupid.Request) bool { - if req.Method != http.MethodGet { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - foldPath := "" - if len(req.Path) > 1 { - foldPath = filepath.Join(req.Path[2:]...) - } - fils, err := os.ReadDir(filepath.Join(d.filesFolder, foldPath)) - if err != nil { - log.Println("Error while getting files:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - out := "" - var inf fs.FileInfo - for _, f := range fils { - if f.IsDir() { - continue - } - inf, err = f.Info() - if err != nil { - log.Println("Error while getting FileInfo for", f.Name(), err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - out += "

" + f.Name() + " " + inf.ModTime().Round(time.Minute).String() + "

\n" - } - _, err = req.Resp.Write(pageWith(out, "Files").json()) - if err != nil { - log.Println("Error while writing output:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - } - return true -} - -type project struct { - ID string `bson:"_id"` - Repository string - Description string - Language []struct { - Language string - Dates string - } -} - -func selectedString(selected bool) string { - if !selected { - return "" - } - return " selected" -} - -func (d *DarkstormTech) handlePortfolio(req *stupid.Request) bool { - if req.Method != http.MethodGet { - req.Resp.WriteHeader(http.StatusBadRequest) - return true - } - filter := bson.M{} - lang := "" - if l, ok := req.Query["lang"]; ok && len(l) == 1 && l[0] != "" { - lang = l[0] - filter = bson.M{"language.language": l[0]} - } - projects := make([]project, 0) - res, err := d.DB.Collection("projects").Find(context.TODO(), filter) - if err != nil { - log.Println("Error while getting projects:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - err = res.All(context.TODO(), &projects) - if err != nil { - log.Println("Error while decoding projects:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - return true - } - out := "

Language Filter:

" - for _, p := range projects { - out += "

" + p.ID + "

" - out += "

" + p.Repository + "

" - for _, l := range p.Language { - lang := l.Language - if lang == "Dart" { - lang = "Dart (Flutter)" - } - out += "

" + lang + ": " + l.Dates + "

" - } - out += "

" + p.Description + "

" - } - _, err = req.Resp.Write(pageWith(out, "Portfolio").json()) - if err != nil { - log.Println("Error while writing output:", err) - req.Resp.WriteHeader(http.StatusInternalServerError) - } - return true -} diff --git a/internal/flexmls/app.go b/internal/flexmls/app.go deleted file mode 100644 index 99c7e62..0000000 --- a/internal/flexmls/app.go +++ /dev/null @@ -1,14 +0,0 @@ -package flexmls - -import ( - "github.com/CalebQ42/stupid-backend/v2/defaultapp" - "go.mongodb.org/mongo-driver/mongo" -) - -type FlexMLS struct { - *defaultapp.App -} - -func NewBackend(client *mongo.Client) *FlexMLS { - return &FlexMLS{defaultapp.NewDefaultApp(client.Database("flexmls"))} -} diff --git a/linker.go b/linker.go deleted file mode 100644 index 617c9c6..0000000 --- a/linker.go +++ /dev/null @@ -1,215 +0,0 @@ -package main - -import ( - "bufio" - "errors" - "io" - "log" - "net" - "os" - "strconv" - "strings" - "time" - - forward "github.com/1lann/udp-forward" -) - -type link struct { - addr string - linkType string -} - -func (l link) isTCP() bool { - return strings.HasPrefix(l.linkType, "tcp") || strings.HasPrefix(l.linkType, "unix") -} - -func (l link) isUDP() bool { - return strings.HasPrefix(l.linkType, "udp") -} - -func linker() { - links, err := parseConf() - if err != nil { - log.Println("Error while trying to parse config file:", err, "tcp linker signing off") - quitChan <- "tcp conf" - return - } else if links == nil { - log.Println("No values in config file or file not present (/etc/darkstorm-server.conf). tcp linker signing off") - quitChan <- "tcp conf" - return - } - fails := make(map[int]int) //logs how many fails per 5 seconds - failChan := make(chan int, 20) - open := make(map[int]bool) - for port, addr := range links { - open[port] = true - go createLink(port, addr, failChan) - } -failWaiting: - for portFail := <-failChan; ; portFail = <-failChan { - if fails[portFail] == 0 { - go func() { - time.Sleep(5 * time.Second) - fails[portFail] = 0 - }() - } else if fails[portFail] == 4 { - log.Println("Port", portFail, "has failed 5 time is as many seconds. Not restarting port...") - open[portFail] = false - for _, b := range open { - if b { - continue failWaiting - } - } - log.Println("All ports dead. Attempting restart...") - quitChan <- "tcp err" - return - } - fails[portFail]++ - log.Println("Restarting linking for port", portFail) - go createLink(portFail, links[portFail], failChan) - } - -} - -func createLink(port int, l link, failChan chan int) { - log.Println("Linking", port, "to", l.addr, "with type", l.linkType) - if l.isUDP() { - _, err := forward.Forward(":"+strconv.Itoa(port), l.addr, forward.DefaultTimeout) - if err != nil { - log.Println("Error with udp forwarder on port", port, ":", err) - failChan <- port - return - } - return - } - var tcpListen net.Listener - var con net.Conn - var err error - if l.isTCP() { - tcpListen, err = net.Listen(l.linkType, ":"+strconv.Itoa(port)) - if err != nil { - log.Println("Error while trying to listen to port", port, ":", err) - failChan <- port - return - } - defer tcpListen.Close() - } - for { - con, err = tcpListen.Accept() - if err != nil { - log.Println("Error while trying to accept connection to port ", port, ":", err) - failChan <- port - return - } - err = copyConn(con, l) - if err != nil { - log.Println("Error while trying copy data from port", port, "to address", l.addr, ":", err) - failChan <- port - return - } - } -} - -func parseConf() (links map[int]link, err error) { - conf, err := os.Open("/etc/darkstorm-server.conf") - if os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - lineNum := 0 - links = make(map[int]link) - rdr := bufio.NewReader(conf) - multilineComment := false - var line string - for { - if line == "" { - lineNum++ - line, err = rdr.ReadString('\n') - if err != nil && line == "" { - break - } else if line == "" { - continue - } - } - startCom, endCom := strings.Index(line, "/*"), strings.Index(line, "*/") - if multilineComment { - if endCom != -1 { - line = line[endCom:] - } else { - continue - } - } - if startCom != -1 { - if endCom != -1 { - line = line[:startCom] + line[endCom:] - continue - } - line = line[:startCom] - multilineComment = true - } - if strings.Contains(line, "//") { - line = line[:strings.Index(line, "//")] - } - line = strings.ReplaceAll(line, "\t", " ") - for strings.Contains(line, " ") { - line = strings.Replace(line, " ", " ", -1) - } - line = strings.TrimSpace(line) - if line == "" { - continue - } - split := strings.Split(line, " ") - if len(split) < 2 || len(split) > 3 { - return nil, errors.New("invalid line #" + strconv.Itoa(lineNum)) - } - var l link - if len(split) == 3 { - l.linkType = split[0] - split = split[1:] - } else { - l.linkType = "tcp" - } - var i int - i, err = strconv.Atoi(split[0]) - if err != nil { - return nil, errors.New("invalid line #" + strconv.Itoa(lineNum)) - } - l.addr = split[1] - links[i] = l - line = "" - } - err = nil - if len(links) == 0 { - return nil, nil - } - return -} - -func copyConn(src net.Conn, l link) error { - dst, err := net.Dial(l.linkType, l.addr) - if err != nil { - log.Println("Error while dialing", l.addr) - return err - } - - done := make(chan struct{}) - - go func() { - defer src.Close() - defer dst.Close() - io.Copy(dst, src) - done <- struct{}{} - }() - - go func() { - defer src.Close() - defer dst.Close() - io.Copy(src, dst) - done <- struct{}{} - }() - - <-done - <-done - return nil -} diff --git a/main.go b/main.go index 9972ee2..da29a2c 100644 --- a/main.go +++ b/main.go @@ -1,48 +1,4 @@ package main -import ( - "flag" - "log" - "time" -) - -var quitChan chan string = make(chan string) - func main() { - mongoStr := flag.String("mongo", "", "MongoDB connection string for APIs") - flag.Parse() - go linker() - go webserver(*mongoStr) - go startSMTPServer() - for failure := <-quitChan; ; failure = <-quitChan { - switch failure { - case "tcp conf": - continue - case "tcp err": - go tcpLinkerRestart() - case "web arg": - continue - case "web err": - go websiteRestart(*mongoStr) - case "smtp arg": - continue - case "smtp err": - //TODO: restart smtp server - continue - } - } -} - -func tcpLinkerRestart() { - log.Println("TCP linker failed. Restarting in 5 seconds...") - time.Sleep(5 * time.Second) - log.Println("Restarting tcp linker") - linker() -} - -func websiteRestart(mongoStr string) { - log.Println("Website failed. Restarting in 5 seconds...") - time.Sleep(5 * time.Second) - log.Println("Restarting website") - webserver(mongoStr) } diff --git a/stupid.go b/stupid.go deleted file mode 100644 index e5f7cee..0000000 --- a/stupid.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "flag" - "io" - "log" - "net/http" - "os" - "path/filepath" - - "github.com/CalebQ42/cdr-backend" - "github.com/CalebQ42/darkstorm-server/internal/darkstormtech" - "github.com/CalebQ42/darkstorm-server/internal/flexmls" - "github.com/CalebQ42/stupid-backend/v2" - "github.com/CalebQ42/stupid-backend/v2/db" - swassistantbackend "github.com/CalebQ42/swassistant-backend" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -func setupStupid(keyPath, mongoStr string) error { - tlsCert, err := tls.LoadX509KeyPair(keyPath+"/fullchain.pem", keyPath+"/key.pem") - if err != nil { - return err - } - client, err := mongo.Connect( - context.TODO(), - options.Client().ApplyURI(mongoStr), - options.Client().SetTLSConfig(&tls.Config{ - Certificates: []tls.Certificate{tlsCert}, - }), - ) - if err != nil { - log.Println("Issues connecting to mongo:", err) - return err - } - stupid := stupid.NewStupidBackend(db.NewMongoTable(client.Database("stupid").Collection("keys")), map[string]any{ - "swassistant": swassistantbackend.NewSWBackend(client), - "cdr": cdr.NewBackend(client), - "darkstormtech": darkstormtech.NewDarkstormTech(client, filepath.Join(flag.Arg(0), "files")), - "flexmls": flexmls.NewBackend(client), - }, "https://darkstorm.tech") - users := true - var pub, priv []byte - stupidPubFil, err := os.Open(keyPath + "/stupid-pub.key") - if err != nil { - log.Println("Disabling API users:", err) - users = false - } else { - pub, err = io.ReadAll(stupidPubFil) - if err != nil { - log.Println("Disabling API users:", err) - users = false - } - } - stupidPrivFil, err := os.Open(keyPath + "/stupid-pub.key") - if err != nil { - log.Println("Disabling API users:", err) - users = false - } else { - priv, err = io.ReadAll(stupidPrivFil) - if err != nil { - log.Println("Disabling API users:", err) - users = false - } - } - if users { - stupid.EnableUserAuth(db.NewMongoTable(client.Database("stupid").Collection("keys")), pub, priv) - } - http.Handle("api.darkstorm.tech/", stupid) - return nil -} diff --git a/web.go b/web.go deleted file mode 100644 index 22dcb21..0000000 --- a/web.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "crypto/tls" - "flag" - "log" - "net/http" - "net/http/httputil" - "net/url" - "os" - "path" - "strings" -) - -func webserver(mongoStr string) { - path := flag.Arg(0) - keyPath := flag.Arg(1) - if path == "" { - log.Println("No argument given for website file path. website signing off...") - quitChan <- "web arg" - return - } else if keyPath == "" { - log.Println("No argument given for key files. website signing off...") - quitChan <- "web arg" - return - } - var err error - if mongoStr != "" { - err = setupStupid(keyPath, mongoStr) - if err != nil { - quitChan <- "web err" - return - } - } - url, err := url.Parse("https://localhost:30000") - if err != nil { - log.Println("Can't parse foundry url:", err) - quitChan <- "web err" - return - } - // http.Handle("/", http.FileServer(http.Dir(path))) - mainHandle := &fileOrIndexHandler{ - baseFolder: path, - appFolders: []string{ - "SWAssistant", - "CDR", - }, - } - http.Handle("/", mainHandle) - // http.Handle("/SWAssistant/", swaHandler{}) - // http.Handle("/CDR/", cdrHandler{}) - http.Handle("rpg.darkstorm.tech/", httputil.NewSingleHostReverseProxy(url)) - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - err = http.ListenAndServeTLS(":443", keyPath+"/fullchain.pem", keyPath+"/key.pem", nil) - log.Println("Error while serving website:", err) - quitChan <- "web err" -} - -type fileOrIndexHandler struct { - baseFolder string - appFolders []string -} - -func (f *fileOrIndexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - reqPath := strings.TrimPrefix(path.Clean(r.URL.Path), "/") - if reqPath == "" || reqPath == "index.html" { - http.ServeFile(w, r, path.Join(f.baseFolder, "index.html")) - return - } - reqPath = path.Join(f.baseFolder, reqPath) - if fil, err := os.Open(reqPath); err == nil { - inf, _ := fil.Stat() - if !inf.IsDir() { - http.ServeFile(w, r, reqPath) - return - } else if _, err = os.Open(path.Join(reqPath, "index.html")); err == nil { - http.ServeFile(w, r, path.Join(reqPath, "index.html")) - return - } - } - for _, a := range f.appFolders { - if strings.HasPrefix(reqPath, path.Join(f.baseFolder, a)) { - http.ServeFile(w, r, path.Join(f.baseFolder, a, "index.html")) - return - } - } - http.ServeFile(w, r, path.Join(f.baseFolder, "index.html")) -} From b6bc9240d9fb44058640880b29eb18099a157d20 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 17 May 2024 22:07:40 -0500 Subject: [PATCH 02/17] Added to docs --- internal/darkstorm_backend/README.md | 58 +++++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 595c91b..9d244c2 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -2,14 +2,44 @@ This is a purposefully "simple" application backend made specifically for _my_ apps. It's purpose is to collect minimal (only what's absolutely necessary) amounts of data while still fulfilling all my needs. I've found that other, off the shelf options such as Firebase are a bit heavy on the data collection. Plus I like to make things :P. +## DB Structure + +### API Key + +```json +{ + appName: "app name", + key: "API Key", + death: -1, // unix timestamp when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) + perm: { + user: true, // create and login users + log: true, // log users + crash: true, // crash reports + // further permissions can be added as needed + } +} +``` + +### User + +```json +{ + id: "UUID", + username: "username", + password: "hashed password", + salt: "password salt", + passwordChange: 0, // unix timestamp of last password change +} +``` + ## Standard Header Any request might or might not need these values. These values can be authenticated via the `TODO` function. ```json { - X-API-Key: "{API Key}", - Authorization: "Bearer {JWT Token}" + X-API-Key: "{API Key}", + Authorization: "Bearer {JWT Token}" } ``` @@ -19,8 +49,8 @@ If an error status code is returned then the body will be as follows. ```json { - errorCode: "Error value for internal use", - errorMsg: "User error message", //This message is meant to be displayed to the user. May be empty. + errorCode: "Error value for internal use", + errorMsg: "User error message", //This message is meant to be displayed to the user. May be empty. } ``` @@ -28,7 +58,7 @@ If an error status code is returned then the body will be as follows. > TODO: Add the ability to create users and log-in through third-parties (such as Google). -All requsests pertaining to users requires the `X-API-Key` header. +All requsests pertaining to users requires the `X-API-Key` header and the key must have the `users` permission. ### Create User @@ -39,9 +69,9 @@ Request: ```json { - username: "Username", - password: "Password", - email: "Email", + username: "Username", + password: "Password", + email: "Email", } ``` @@ -49,8 +79,8 @@ Return: ```json { - username: "Username", - token: "JWT Token" + username: "Username", + token: "JWT Token" } ``` @@ -71,8 +101,8 @@ Request: ```json { - username: "Username", - password: "Password", + username: "Username", + password: "Password", } ``` @@ -80,7 +110,7 @@ Return: ```json { - token: "JWT Token", - timeout: 0, + token: "JWT Token", + timeout: 0, // login attempt timeout (in seconds). If non-zero, token will be empty. } ``` From 75f83568938f4c6f70e79ce27612ebf197c30c49 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sat, 18 May 2024 10:00:37 -0500 Subject: [PATCH 03/17] Getting basics down --- go.mod | 7 +++ go.sum | 6 ++ internal/darkstorm_backend/README.md | 4 ++ internal/darkstorm_backend/app.go | 10 +++- internal/darkstorm_backend/crash.go | 4 ++ internal/darkstorm_backend/darkstorm_test.go | 1 + internal/darkstorm_backend/user.go | 62 ++++++++++++++++++++ internal/db/interface.go | 5 ++ 8 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 internal/darkstorm_backend/crash.go create mode 100644 internal/darkstorm_backend/darkstorm_test.go create mode 100644 internal/darkstorm_backend/user.go create mode 100644 internal/db/interface.go diff --git a/go.mod b/go.mod index e88cfaf..3b03944 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/CalebQ42/darkstorm-server go 1.22.3 + +require golang.org/x/crypto v0.23.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..4297843 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 9d244c2..35e8bbf 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -28,7 +28,11 @@ This is a purposefully "simple" application backend made specifically for _my_ a username: "username", password: "hashed password", salt: "password salt", + email: "email", passwordChange: 0, // unix timestamp of last password change + perm: { + appID: "user", // Optional. Apps should have a default permission level if thier appID is not in perm. + } } ``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index a1e2aca..4ebdc4b 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -1,3 +1,11 @@ package darkstorm -type App interface{} \ No newline at end of file +type App interface { + //TODO +} + +type CrashApp interface { + App + AddCrash(CrashReport) + // TODO +} diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go new file mode 100644 index 0000000..043f315 --- /dev/null +++ b/internal/darkstorm_backend/crash.go @@ -0,0 +1,4 @@ +package darkstorm + +type CrashReport struct{} + diff --git a/internal/darkstorm_backend/darkstorm_test.go b/internal/darkstorm_backend/darkstorm_test.go new file mode 100644 index 0000000..a8fface --- /dev/null +++ b/internal/darkstorm_backend/darkstorm_test.go @@ -0,0 +1 @@ +package darkstorm diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go new file mode 100644 index 0000000..439299d --- /dev/null +++ b/internal/darkstorm_backend/user.go @@ -0,0 +1,62 @@ +package darkstorm + +import ( + "crypto/rand" + "encoding/base64" + + "github.com/google/uuid" + "golang.org/x/crypto/argon2" +) + +func generateSalt() (string, error) { + out := make([]byte, 16) + _, err := rand.Read(out) + return base64.RawStdEncoding.EncodeToString(out), err +} + +type User struct { + Perm map[string]string + ID string + Username string + Password string + Salt string + Email string + PasswordChange uint64 +} + +func NewUser(username, password, email string) (*User, error) { + id, err := uuid.NewV7() + if err != nil { + return nil, err + } + salt, err := generateSalt() + if err != nil { + return nil, err + } + out := &User{ + Perm: make(map[string]string), + ID: id.String(), + Username: username, + Salt: salt, + Email: email, + } + out.Password, err = out.HashPassword(password) + return out, err +} + +func (u *User) HashPassword(password string) (string, error) { + salt, err := base64.RawStdEncoding.DecodeString(u.Salt) + if err != nil { + return "", err + } + res := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) + return base64.RawStdEncoding.EncodeToString(res), nil +} + +func (u *User) ValidatePassword(password string) (bool, error) { + hsh, err := u.HashPassword(password) + if err != nil { + return false, err + } + return hsh == u.Password, nil +} diff --git a/internal/db/interface.go b/internal/db/interface.go new file mode 100644 index 0000000..8a2770a --- /dev/null +++ b/internal/db/interface.go @@ -0,0 +1,5 @@ +package db + +type DB interface { + //TODO +} From f7bbdaa4b32635c3ab79aa2408b922db466da5d9 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sat, 18 May 2024 22:07:24 -0500 Subject: [PATCH 04/17] Filling out docs some more --- go.mod | 8 +- internal/darkstorm_backend/README.md | 78 +++++++++++++++++--- internal/darkstorm_backend/crash.go | 13 +++- internal/darkstorm_backend/darkstorm_test.go | 9 +++ 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 3b03944..4c16ab9 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,9 @@ module github.com/CalebQ42/darkstorm-server go 1.22.3 -require golang.org/x/crypto v0.23.0 - require ( - github.com/google/uuid v1.6.0 // indirect - golang.org/x/sys v0.20.0 // indirect + github.com/google/uuid v1.6.0 + golang.org/x/crypto v0.23.0 ) + +require golang.org/x/sys v0.20.0 // indirect diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 35e8bbf..10d99db 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -8,8 +8,8 @@ This is a purposefully "simple" application backend made specifically for _my_ a ```json { - appName: "app name", - key: "API Key", + id: "API Key", + appID: "appID", death: -1, // unix timestamp when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) perm: { user: true, // create and login users @@ -22,6 +22,8 @@ This is a purposefully "simple" application backend made specifically for _my_ a ### User +Users are stored per backend and not per app. + ```json { id: "UUID", @@ -36,18 +38,46 @@ This is a purposefully "simple" application backend made specifically for _my_ a } ``` -## Standard Header +### Crash Reports -Any request might or might not need these values. These values can be authenticated via the `TODO` function. +#### Individual Report + +```json +{ + count: 1, // We do not store duplicates. If a duplicate does occur + platform: "android", + error: "error", + stack: "stacktrace" +} +``` + +#### Crashes + +```json +{ + id: "UUID", + error: "error", + firstLine: "first line of error", + individual: [ + // Individual Crash Reports + ] +} +``` + +## Requests + +### Standard Header + +Any request might or might not need these headers. These values can be authenticated via the `ParseHeader` function. ```json { X-API-Key: "{API Key}", - Authorization: "Bearer {JWT Token}" + Authorization: "Bearer {JWT Token}" // No built-in functions require a JWT Token, but may be required by specific implementations. } ``` -## Error Response +### Error Response If an error status code is returned then the body will be as follows. @@ -58,23 +88,26 @@ If an error status code is returned then the body will be as follows. } ``` -## Users +### Users > TODO: Add the ability to create users and log-in through third-parties (such as Google). All requsests pertaining to users requires the `X-API-Key` header and the key must have the `users` permission. -### Create User +#### Create User > TODO: Email user to confirm. +> > TODO: Screen username for offensive words and phrases. Request: +> POST: /user/create + ```json { username: "Username", - password: "Password", + password: "Password", // Password must be email: "Email", } ``` @@ -90,19 +123,23 @@ Return: If returned status is 401, the errorCode will be one of the following: -* username +* usernameTaken * Username is already taken +* usernameDisallowed + * Username is not allowed (due to offensive words/phrases) * password - * Password does not follow rules + * Password is to short or too long. * email * Email is already linked to an account * disallowed * Username contains words/phases that are not allowed -### Login +#### Login Request: +> POST: /user + ```json { username: "Username", @@ -118,3 +155,20 @@ Return: timeout: 0, // login attempt timeout (in seconds). If non-zero, token will be empty. } ``` + +### Crash Report + +Crash reports require the `X-API-Key` header and the key must match the URL's appID and have the `crash` permission + +Request: + +> POST: /{appID}/crash + +```json +{ + id: "UUID", // This is an ignored value, but it is highly recommended to include it to prevent reporting the same crash multiple times. + platform: "android", + error: "error", + stack: "stacktrace" +} +``` diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 043f315..cc87fa2 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -1,4 +1,15 @@ package darkstorm -type CrashReport struct{} +type IndividualCrash struct { + Platform string + Error string + Stack string + Count int +} +type CrashReport struct { + ID string + Error string + FirstLine string + Individual []IndividualCrash +} diff --git a/internal/darkstorm_backend/darkstorm_test.go b/internal/darkstorm_backend/darkstorm_test.go index a8fface..53758c2 100644 --- a/internal/darkstorm_backend/darkstorm_test.go +++ b/internal/darkstorm_backend/darkstorm_test.go @@ -1 +1,10 @@ package darkstorm + +import ( + "os" + "testing" +) + +func TestMain(t *testing.M) { + os.Exit(t.Run()) +} From 4244b6985a3a379257ab67b5a9c2962a4200d324 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sun, 19 May 2024 06:47:41 -0500 Subject: [PATCH 05/17] Mostly finish docs Actually start to parse things --- go.mod | 5 +- go.sum | 2 + internal/darkstorm_backend/README.md | 37 ++++++++++- internal/darkstorm_backend/app.go | 9 +-- internal/darkstorm_backend/crash.go | 39 +++++++++++ internal/darkstorm_backend/darkstorm.go | 87 ++++++++++++++++++++++++- internal/darkstorm_backend/db.go | 23 +++++++ internal/darkstorm_backend/key.go | 12 ++++ internal/darkstorm_backend/log.go | 11 ++++ internal/darkstorm_backend/user.go | 60 ++++++++++++++++- internal/db/interface.go | 5 -- 11 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 internal/darkstorm_backend/db.go create mode 100644 internal/darkstorm_backend/key.go create mode 100644 internal/darkstorm_backend/log.go delete mode 100644 internal/db/interface.go diff --git a/go.mod b/go.mod index 4c16ab9..9131155 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,7 @@ require ( golang.org/x/crypto v0.23.0 ) -require golang.org/x/sys v0.20.0 // indirect +require ( + github.com/golang-jwt/jwt/v5 v5.2.1 + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum index 4297843..c5c1062 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 10d99db..5763724 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -15,11 +15,22 @@ This is a purposefully "simple" application backend made specifically for _my_ a user: true, // create and login users log: true, // log users crash: true, // crash reports + management: false, // managing // further permissions can be added as needed } } ``` +### DB Log + +```json +{ + id: "UUID", + platform: "android", + Date: 20240519 // YYYYMMD +} +``` + ### User Users are stored per backend and not per app. @@ -88,6 +99,14 @@ If an error status code is returned then the body will be as follows. } ``` +### Log + +API Key must have the `log` permission. + +Request: + +> POST: /log + ### Users > TODO: Add the ability to create users and log-in through third-parties (such as Google). @@ -107,7 +126,7 @@ Request: ```json { username: "Username", - password: "Password", // Password must be + password: "Password", // Allowed length: 12-128 email: "Email", } ``` @@ -158,11 +177,15 @@ Return: ### Crash Report -Crash reports require the `X-API-Key` header and the key must match the URL's appID and have the `crash` permission +> TODO: Archive a crash to prevent it being reported again. + +#### Report + +API Key must have the `crash` permission. Request: -> POST: /{appID}/crash +> POST: /crash ```json { @@ -172,3 +195,11 @@ Request: stack: "stacktrace" } ``` + +#### Delete + +API Key must have the `management` permission. + +Request: + +> DELETE: /crash/{crashID} diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index 4ebdc4b..96d9108 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -1,11 +1,6 @@ package darkstorm type App interface { - //TODO -} - -type CrashApp interface { - App - AddCrash(CrashReport) - // TODO + LogTable() Table[Log] + CrashTable() CrashTable } diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index cc87fa2..56cb458 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -1,5 +1,7 @@ package darkstorm +import "net/http" + type IndividualCrash struct { Platform string Error string @@ -13,3 +15,40 @@ type CrashReport struct { FirstLine string Individual []IndividualCrash } + +func (c CrashReport) GetID() string { + return c.ID +} + +type crashReq struct { + ID string + Platform string + Error string + Stack string +} + +func (b *Backend) ReportCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["crash"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} + +func (b *Backend) DeleteCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["management"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 8b49e57..efa85bc 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -1,7 +1,90 @@ package darkstorm -import "net/http" +import ( + "crypto/ed25519" + "errors" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + ErrApiKeyUnauthorized = errors.New("api key invalid") + ErrTokenUnauthorized = errors.New("token invalid") +) type Backend struct { - http.ServeMux + userTable Table[User] + keyTable Table[Key] + m *http.ServeMux + jwtPriv ed25519.PrivateKey + jwtPub ed25519.PublicKey + apps []App +} + +func NewBackend(keyTable Table[Key], apps ...App) (*Backend, error) { + b := &Backend{ + keyTable: keyTable, + m: &http.ServeMux{}, + apps: apps, + } + //TODO: register paths to the mux + b.startCleanupLoop() + return b, nil +} + +func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { + b.userTable = userTable + b.jwtPriv = privKey + b.jwtPub = pubKey +} + +func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) { + b.m.HandleFunc(pattern, h) +} + +func (b *Backend) startCleanupLoop() { + go func() { + for range time.Tick(6 * time.Hour) { + //TODO + } + }() +} + +type ParsedHeader struct { + u *ReqUser + k *Key +} + +func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { + out := ParsedHeader{} + key := r.Header.Get("X-API-Key") + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if key != "" { + apiKey, err := b.keyTable.Get(key) + if err != nil { + return out, errors.Join(ErrApiKeyUnauthorized, err) + } + out.k = &apiKey + } + if token != "" && b.userTable != nil { + t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return b.jwtPub, nil + }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired()) + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + sub, err := t.Claims.GetSubject() + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + usr, err := b.userTable.Get(sub) + if err != nil{ + return out, errors.Join(ErrTokenUnauthorized, err) + } + + } + return out, nil } diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go new file mode 100644 index 0000000..87a16be --- /dev/null +++ b/internal/darkstorm_backend/db.go @@ -0,0 +1,23 @@ +package darkstorm + +import "errors" + +var ( + ErrIDNotFound = errors.New("id not found in table") +) + +type IDStruct interface { + GetID() string +} + +type Table[T IDStruct] interface { + Get(ID string) (data T, err error) + Insert(data T) error + Update(data T) error + Remove(ID string) +} + +type CrashTable interface { + Table[CrashReport] + InsertCrash(report IndividualCrash) error +} diff --git a/internal/darkstorm_backend/key.go b/internal/darkstorm_backend/key.go new file mode 100644 index 0000000..74dd5e9 --- /dev/null +++ b/internal/darkstorm_backend/key.go @@ -0,0 +1,12 @@ +package darkstorm + +type Key struct { + Perm map[string]bool + ID string + AppID string + Death int +} + +func (k Key) GetID() string { + return k.ID +} diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go new file mode 100644 index 0000000..4976e51 --- /dev/null +++ b/internal/darkstorm_backend/log.go @@ -0,0 +1,11 @@ +package darkstorm + +type Log struct { + ID string + Platform string + Date int +} + +func (l Log) GetID() string { + return l.ID +} diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index 439299d..a456306 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -3,11 +3,17 @@ package darkstorm import ( "crypto/rand" "encoding/base64" + "errors" + "net/http" "github.com/google/uuid" "golang.org/x/crypto/argon2" ) +var ( + ErrPasswordLength = errors.New("password length must be 12-128") +) + func generateSalt() (string, error) { out := make([]byte, 16) _, err := rand.Read(out) @@ -24,7 +30,16 @@ type User struct { PasswordChange uint64 } +type ReqUser struct { + Perm map[string]string + ID string + Username string +} + func NewUser(username, password, email string) (*User, error) { + if len(password) < 12 || len(password) > 128 { + return nil, ErrPasswordLength + } id, err := uuid.NewV7() if err != nil { return nil, err @@ -44,7 +59,19 @@ func NewUser(username, password, email string) (*User, error) { return out, err } -func (u *User) HashPassword(password string) (string, error) { +func (u User) GetID() string { + return u.ID +} + +func (u User) toReqUser() ReqUser { + return ReqUser{ + Perm: u.Perm, + ID: u.ID, + Username: u.Username, + } +} + +func (u User) HashPassword(password string) (string, error) { salt, err := base64.RawStdEncoding.DecodeString(u.Salt) if err != nil { return "", err @@ -53,10 +80,39 @@ func (u *User) HashPassword(password string) (string, error) { return base64.RawStdEncoding.EncodeToString(res), nil } -func (u *User) ValidatePassword(password string) (bool, error) { +func (u User) ValidatePassword(password string) (bool, error) { hsh, err := u.HashPassword(password) if err != nil { return false, err } return hsh == u.Password, nil } + +type createUserRequest struct { + Username string + Password string + Email string +} + +type createUserReturn struct { + Username string + Token string +} + +func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { + //TODO +} + +type loginRequest struct { + Username string + Password string +} + +type loginReturn struct { + Token string + Timeout int +} + +func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { + //TODO +} diff --git a/internal/db/interface.go b/internal/db/interface.go deleted file mode 100644 index 8a2770a..0000000 --- a/internal/db/interface.go +++ /dev/null @@ -1,5 +0,0 @@ -package db - -type DB interface { - //TODO -} From e4f8b31e2927c54f7e252e2c5dc9d48ac13d15ac Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Tue, 21 May 2024 05:50:07 -0500 Subject: [PATCH 06/17] (Probably) finished with docs and interfaces. Starting to build out the actual logic. --- internal/darkstorm_backend/README.md | 53 +++++++++++- internal/darkstorm_backend/app.go | 10 +++ internal/darkstorm_backend/crash.go | 23 +++++- internal/darkstorm_backend/darkstorm.go | 102 ++++++++++++------------ internal/darkstorm_backend/db.go | 10 ++- internal/darkstorm_backend/header.go | 65 +++++++++++++++ internal/darkstorm_backend/key.go | 6 +- internal/darkstorm_backend/log.go | 6 ++ internal/darkstorm_backend/user.go | 56 ++++++------- 9 files changed, 235 insertions(+), 96 deletions(-) create mode 100644 internal/darkstorm_backend/header.go diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 5763724..4cfe9cb 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -6,11 +6,13 @@ This is a purposefully "simple" application backend made specifically for _my_ a ### API Key +The special appID "darkstormManagement" is used to manage all apps. + ```json { id: "API Key", appID: "appID", - death: -1, // unix timestamp when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) + death: -1, // unix timestamp (seconds) when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) perm: { user: true, // create and login users log: true, // log users @@ -37,12 +39,12 @@ Users are stored per backend and not per app. ```json { - id: "UUID", + id: "uuid", username: "username", password: "hashed password", salt: "password salt", email: "email", - passwordChange: 0, // unix timestamp of last password change + passwordChange: 0, // unix timestamp (seconds) of last password change perm: { appID: "user", // Optional. Apps should have a default permission level if thier appID is not in perm. } @@ -99,13 +101,18 @@ If an error status code is returned then the body will be as follows. } ``` +`errorCode`'s returned from the main library: + +* invalidKey + * API Key is invalid or does not have the needed permission for the request. + ### Log API Key must have the `log` permission. Request: -> POST: /log +> POST: /log/{uuid} ### Users @@ -171,10 +178,20 @@ Return: ```json { token: "JWT Token", + error: "Error", timeout: 0, // login attempt timeout (in seconds). If non-zero, token will be empty. } ``` +`token` and `error` are mutually exclusive. + +Possible `error` values: + +* timeout + * Account is currently timed-out. The `timeout` value will be non-zero. +* invalid + * Either the username or password is incorrect + ### Crash Report > TODO: Archive a crash to prevent it being reported again. @@ -187,6 +204,8 @@ Request: > POST: /crash +Request Body: + ```json { id: "UUID", // This is an ignored value, but it is highly recommended to include it to prevent reporting the same crash multiple times. @@ -203,3 +222,29 @@ API Key must have the `management` permission. Request: > DELETE: /crash/{crashID} + +With "darkstormManagement" key: + +> DELETE: /{appID}/crash/{crashID} + +#### Archive + +Archive an error, preventing error with these values to be ignored in the future. API Key must have the `management` permission. + +Request: + +> POST: /crash/archive + +With "darkstormManagement" key: + +> POST: /{appID}/crash/{crashID} + +Request Body: + +```json +{ + error: "error", + stack: "full stacktrace", + platform: "all", // Limit the archive to a specific platform, or use "all". +} +``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index 96d9108..dd33d16 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -1,6 +1,16 @@ package darkstorm +import "net/http" + +// An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden. type App interface { + AppID() string LogTable() Table[Log] CrashTable() CrashTable } + +type ExtendedApp interface { + // Extension is called for any calls to /{appID}/ + // Alternatively, use Backend.HandleFunc for more customizability + Extension(http.ResponseWriter, *http.Request) +} diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 56cb458..5a1dade 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -2,6 +2,12 @@ package darkstorm import "net/http" +type ArchivedCrash struct { + Error string + Stack string + Platform string +} + type IndividualCrash struct { Platform string Error string @@ -27,7 +33,7 @@ type crashReq struct { Stack string } -func (b *Backend) ReportCrash(w http.ResponseWriter, r *http.Request) { +func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) if hdr.k == nil || hdr.k.Perm["crash"] { w.WriteHeader(http.StatusUnauthorized) @@ -40,7 +46,20 @@ func (b *Backend) ReportCrash(w http.ResponseWriter, r *http.Request) { //TODO } -func (b *Backend) DeleteCrash(w http.ResponseWriter, r *http.Request) { +func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["management"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} + +func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) if hdr.k == nil || hdr.k.Perm["management"] { w.WriteHeader(http.StatusUnauthorized) diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index efa85bc..8ba768d 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -2,39 +2,66 @@ package darkstorm import ( "crypto/ed25519" + "encoding/json" "errors" "net/http" - "strings" "time" - - "github.com/golang-jwt/jwt/v5" -) - -var ( - ErrApiKeyUnauthorized = errors.New("api key invalid") - ErrTokenUnauthorized = errors.New("token invalid") ) type Backend struct { userTable Table[User] - keyTable Table[Key] + keyTable Table[ApiKey] m *http.ServeMux jwtPriv ed25519.PrivateKey jwtPub ed25519.PublicKey - apps []App + apps map[string]App } -func NewBackend(keyTable Table[Key], apps ...App) (*Backend, error) { +func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { b := &Backend{ keyTable: keyTable, m: &http.ServeMux{}, - apps: apps, + apps: make(map[string]App), + } + var hasLog, hasCrash bool + for i := range apps { + _, has := b.apps[apps[i].AppID()] + if has { + return nil, errors.New("duplicate AppIDs found") + } + b.apps[apps[i].AppID()] = apps[i] + if ext, is := apps[i].(ExtendedApp); is { + b.m.HandleFunc("/"+apps[i].AppID()+"/", ext.Extension) + } + if !hasLog && apps[i].LogTable() != nil { + hasLog = true + } + if !hasCrash && apps[i].CrashTable() != nil { + hasCrash = true + } + } + if hasLog { + b.m.HandleFunc("POST /log/{uuid}", b.log) + } + if hasCrash { + b.m.HandleFunc("POST /crash", b.reportCrash) + b.m.HandleFunc("DELETE /crash/{crashID}", b.deleteCrash) + b.m.HandleFunc("POST /crash/archive", b.archiveCrash) + b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.deleteCrash) + b.m.HandleFunc("POST /{appID}/crash/archive", b.archiveCrash) } - //TODO: register paths to the mux b.startCleanupLoop() return b, nil } +func (b *Backend) startCleanupLoop() { + go func() { + for range time.Tick(24 * time.Hour) { + //TODO + } + }() +} + func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { b.userTable = userTable b.jwtPriv = privKey @@ -45,46 +72,19 @@ func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) { b.m.HandleFunc(pattern, h) } -func (b *Backend) startCleanupLoop() { - go func() { - for range time.Tick(6 * time.Hour) { - //TODO - } - }() +func (b *Backend) GetApp(a *ApiKey) App { + return b.apps[a.AppID] } -type ParsedHeader struct { - u *ReqUser - k *Key +type retError struct { + ErrorCode string `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` } -func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { - out := ParsedHeader{} - key := r.Header.Get("X-API-Key") - token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - if key != "" { - apiKey, err := b.keyTable.Get(key) - if err != nil { - return out, errors.Join(ErrApiKeyUnauthorized, err) - } - out.k = &apiKey - } - if token != "" && b.userTable != nil { - t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { - return b.jwtPub, nil - }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired()) - if err != nil { - return out, errors.Join(ErrTokenUnauthorized, err) - } - sub, err := t.Claims.GetSubject() - if err != nil { - return out, errors.Join(ErrTokenUnauthorized, err) - } - usr, err := b.userTable.Get(sub) - if err != nil{ - return out, errors.Join(ErrTokenUnauthorized, err) - } - - } - return out, nil +func ReturnError(w http.ResponseWriter, status int, code, msg string) { + w.WriteHeader(status) + json.NewEncoder(w).Encode(retError{ + ErrorCode: code, + ErrorMsg: msg, + }) } diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index 87a16be..0a9e9ac 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -3,7 +3,7 @@ package darkstorm import "errors" var ( - ErrIDNotFound = errors.New("id not found in table") + ErrNotFound = errors.New("no matches found in table") ) type IDStruct interface { @@ -12,12 +12,14 @@ type IDStruct interface { type Table[T IDStruct] interface { Get(ID string) (data T, err error) + Find(values map[string]any) ([]T, error) Insert(data T) error - Update(data T) error - Remove(ID string) + Remove(ID string) error + FullUpdate(ID string, data T) error + PartUpdate(ID string, update map[string]any) error } type CrashTable interface { Table[CrashReport] - InsertCrash(report IndividualCrash) error + InsertCrash(ID string, report IndividualCrash) error } diff --git a/internal/darkstorm_backend/header.go b/internal/darkstorm_backend/header.go new file mode 100644 index 0000000..924304e --- /dev/null +++ b/internal/darkstorm_backend/header.go @@ -0,0 +1,65 @@ +package darkstorm + +import ( + "errors" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + ErrApiKeyUnauthorized = errors.New("api key present but invalid") + ErrTokenUnauthorized = errors.New("token present but invalid") +) + +type ParsedHeader struct { + u *ReqUser + k *ApiKey +} + +func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { + out := ParsedHeader{} + key := r.Header.Get("X-API-Key") + token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if key != "" { + apiKey, err := b.keyTable.Get(key) + if err != nil { + return out, errors.Join(ErrApiKeyUnauthorized, err) + } + if apiKey.Death > 0 && time.Unix(apiKey.Death, 0).Before(time.Now()) { + return out, ErrApiKeyUnauthorized + } + out.k = &apiKey + } + if token != "" && b.userTable != nil { + t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { + return b.jwtPub, nil + }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired()) + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + exp, _ := t.Claims.GetExpirationTime() + if exp.Time.Before(time.Now()) { + return out, ErrTokenUnauthorized + } + sub, err := t.Claims.GetSubject() + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + usr, err := b.userTable.Get(sub) + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + iss, err := t.Claims.GetIssuedAt() + if err != nil { + return out, errors.Join(ErrTokenUnauthorized, err) + } + if usr.PasswordChange > 0 && iss.Time.Before(time.Unix(usr.PasswordChange, 0)) { + return out, ErrTokenUnauthorized + } + out.u = usr.toReqUser() + } + return out, nil +} diff --git a/internal/darkstorm_backend/key.go b/internal/darkstorm_backend/key.go index 74dd5e9..0fca0fd 100644 --- a/internal/darkstorm_backend/key.go +++ b/internal/darkstorm_backend/key.go @@ -1,12 +1,12 @@ package darkstorm -type Key struct { +type ApiKey struct { Perm map[string]bool ID string AppID string - Death int + Death int64 } -func (k Key) GetID() string { +func (k ApiKey) GetID() string { return k.ID } diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go index 4976e51..309fb48 100644 --- a/internal/darkstorm_backend/log.go +++ b/internal/darkstorm_backend/log.go @@ -1,5 +1,7 @@ package darkstorm +import "net/http" + type Log struct { ID string Platform string @@ -9,3 +11,7 @@ type Log struct { func (l Log) GetID() string { return l.ID } + +func (b *Backend) log(w http.ResponseWriter, r *http.Request) { + //TODO +} diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index a456306..c226f5e 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -5,8 +5,9 @@ import ( "encoding/base64" "errors" "net/http" + "time" - "github.com/google/uuid" + "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/argon2" ) @@ -21,13 +22,13 @@ func generateSalt() (string, error) { } type User struct { - Perm map[string]string - ID string - Username string - Password string - Salt string - Email string - PasswordChange uint64 + Perm map[string]string `json:"perm" bson:"perm"` + ID string `json:"id" bson:"_id"` + Username string `json:"username" bson:"username"` + Password string `json:"password" bson:"password"` + Salt string `json:"salt" bson:"salt"` + Email string `json:"email" bson:"email"` + PasswordChange int64 `json:"passwordChange" bson:"passwordChange"` } type ReqUser struct { @@ -36,35 +37,21 @@ type ReqUser struct { Username string } -func NewUser(username, password, email string) (*User, error) { - if len(password) < 12 || len(password) > 128 { - return nil, ErrPasswordLength - } - id, err := uuid.NewV7() - if err != nil { - return nil, err - } - salt, err := generateSalt() - if err != nil { - return nil, err - } - out := &User{ - Perm: make(map[string]string), - ID: id.String(), - Username: username, - Salt: salt, - Email: email, - } - out.Password, err = out.HashPassword(password) - return out, err +func (b *Backend) generateJWT(r *ReqUser) (string, error) { + return jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{ + ID: r.ID, + Issuer: "darkstorm.tech", + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * time.Hour)), + }).SignedString(b.jwtPriv) } func (u User) GetID() string { return u.ID } -func (u User) toReqUser() ReqUser { - return ReqUser{ +func (u User) toReqUser() *ReqUser { + return &ReqUser{ Perm: u.Perm, ID: u.ID, Username: u.Username, @@ -114,5 +101,10 @@ type loginReturn struct { } func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { - //TODO + hdr, err := b.ParseHeader(r) + if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return + } + } From 144f45293bc1b449c852e3a2ec7c4952ff04c60a Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 24 May 2024 04:26:24 -0500 Subject: [PATCH 07/17] User login and creation --- internal/darkstorm_backend/README.md | 18 +-- internal/darkstorm_backend/darkstorm.go | 11 +- internal/darkstorm_backend/header.go | 3 + internal/darkstorm_backend/user.go | 142 +++++++++++++++++++++++- 4 files changed, 156 insertions(+), 18 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 4cfe9cb..4239662 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -6,7 +6,7 @@ This is a purposefully "simple" application backend made specifically for _my_ a ### API Key -The special appID "darkstormManagement" is used to manage all apps. +The special appID "darkstormManagement" is used to manage all apps. ```json { @@ -44,6 +44,8 @@ Users are stored per backend and not per app. password: "hashed password", salt: "password salt", email: "email", + fails: 0, // number of failed attemps in a row. + timeout: 0, // unix timestamp (seconds) when current timeout ends. passwordChange: 0, // unix timestamp (seconds) of last password change perm: { appID: "user", // Optional. Apps should have a default permission level if thier appID is not in perm. @@ -105,6 +107,10 @@ If an error status code is returned then the body will be as follows. * invalidKey * API Key is invalid or does not have the needed permission for the request. +* invalidBody + * Body of the request is malformed. +* internal + * Server-side issue. ### Log @@ -149,16 +155,12 @@ Return: If returned status is 401, the errorCode will be one of the following: -* usernameTaken - * Username is already taken +* taken + * Username or email is already taken * usernameDisallowed * Username is not allowed (due to offensive words/phrases) * password * Password is to short or too long. -* email - * Email is already linked to an account -* disallowed - * Username contains words/phases that are not allowed #### Login @@ -179,7 +181,7 @@ Return: { token: "JWT Token", error: "Error", - timeout: 0, // login attempt timeout (in seconds). If non-zero, token will be empty. + timeout: 0, // login attempt timeout remaining (in seconds). If non-zero, token will be empty. } ``` diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 8ba768d..d93766a 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "net/http" + "sync" "time" ) @@ -12,16 +13,18 @@ type Backend struct { userTable Table[User] keyTable Table[ApiKey] m *http.ServeMux + apps map[string]App jwtPriv ed25519.PrivateKey jwtPub ed25519.PublicKey - apps map[string]App + userMutex sync.RWMutex } func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { b := &Backend{ - keyTable: keyTable, - m: &http.ServeMux{}, - apps: make(map[string]App), + keyTable: keyTable, + m: &http.ServeMux{}, + apps: make(map[string]App), + userMutex: sync.RWMutex{}, } var hasLog, hasCrash bool for i := range apps { diff --git a/internal/darkstorm_backend/header.go b/internal/darkstorm_backend/header.go index 924304e..60d4b8a 100644 --- a/internal/darkstorm_backend/header.go +++ b/internal/darkstorm_backend/header.go @@ -19,6 +19,9 @@ type ParsedHeader struct { k *ApiKey } +// Parses the X-API-Key and Authorization headers. If the API Key provided but invalid (either due to expiring or isn't found), +// ErrApiKeyUnauthorized is part of the returned error (check with errors.Is). +// If the Authorization header is present but invalid, ErrTokenUnauthorized is part of the returned error (check with errors.Is). func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { out := ParsedHeader{} key := r.Header.Get("X-API-Key") diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index c226f5e..5b5699a 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -3,11 +3,13 @@ package darkstorm import ( "crypto/rand" "encoding/base64" + "encoding/json" "errors" "net/http" "time" "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" "golang.org/x/crypto/argon2" ) @@ -28,9 +30,34 @@ type User struct { Password string `json:"password" bson:"password"` Salt string `json:"salt" bson:"salt"` Email string `json:"email" bson:"email"` + Fails int `json:"fails" bson:"fails"` + Timeout int64 `json:"timeout" bson:"timeout"` PasswordChange int64 `json:"passwordChange" bson:"passwordChange"` } +func NewUser(username, password, email string) (User, error) { + id, err := uuid.NewV7() + if err != nil { + return User{}, err + } + salt, err := generateSalt() + if err != nil { + return User{}, err + } + u := User{ + Perm: make(map[string]string), + ID: id.String(), + Username: username, + Salt: salt, + Email: email, + } + u.Password, err = u.HashPassword(password) + if err != nil { + return u, err + } + return u, nil +} + type ReqUser struct { Perm map[string]string ID string @@ -82,12 +109,65 @@ type createUserRequest struct { } type createUserReturn struct { - Username string - Token string + Username string `json:"username"` + Token string `json:"token"` } func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { - //TODO + hdr, err := b.ParseHeader(r) + if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return + } + defer r.Body.Close() + var req createUserRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil || req.Username == "" || req.Password == "" || req.Email == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") + return + } + if len(req.Password) < 12 || len(req.Password) > 128 { + ReturnError(w, http.StatusUnauthorized, "password", "Invalid password.") + return + } + // TODO: filter offensive words/phrases + b.userMutex.Lock() + defer b.userMutex.Unlock() + matchUsername, err := b.userTable.Find(map[string]any{"username": req.Username}) + if err != nil && !errors.Is(err, ErrNotFound) { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } else if (err == nil || errors.Is(err, ErrNotFound)) && len(matchUsername) > 0 { + ReturnError(w, http.StatusUnauthorized, "taken", "Username or email already used") + return + } + matchEmail, err := b.userTable.Find(map[string]any{"email": req.Email}) + if err != nil && !errors.Is(err, ErrNotFound) { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } else if (err == nil || errors.Is(err, ErrNotFound)) && len(matchEmail) > 0 { + ReturnError(w, http.StatusUnauthorized, "taken", "Username or email already used") + return + } + u, err := NewUser(req.Username, req.Password, req.Email) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + err = b.userTable.Insert(u) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + var ret createUserReturn + ret.Username = u.Username + ret.Token, err = b.generateJWT(u.toReqUser()) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(ret) } type loginRequest struct { @@ -96,8 +176,9 @@ type loginRequest struct { } type loginReturn struct { - Token string - Timeout int + Token string `json:"token"` + Error string `json:"error"` + Timeout int64 `json:"timeout"` } func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { @@ -106,5 +187,54 @@ func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return } - + defer r.Body.Close() + var req loginRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil || req.Username == "" || req.Password == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") + return + } + b.userMutex.RLock() + defer b.userMutex.RUnlock() + var ret loginReturn + users, err := b.userTable.Find(map[string]any{"username": req.Username}) + if errors.Is(err, ErrNotFound) || len(users) != 1 { + ret.Error = "invalid" + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(ret) + return + } + u := users[0] + if time.Unix(u.Timeout, 0).After(time.Now()) { + ret.Error = "timeout" + ret.Timeout = time.Now().Unix() - u.Timeout + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(ret) + return + } + hash, err := u.HashPassword(req.Password) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + if u.Password == hash { + ret.Token, err = b.generateJWT(u.toReqUser()) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + json.NewEncoder(w).Encode(ret) + } else { + ret.Error = "invalid" + upd := map[string]any{"fails": u.Fails + 1} + if (u.Fails+1)%3 == 0 { + minutes := 3 ^ ((u.Fails / 3) - 1) + timeout := time.Now().Add(time.Duration(minutes) * time.Minute).Unix() + upd["timeout"] = timeout + ret.Timeout = timeout - time.Now().Unix() + } + b.userTable.PartUpdate(u.ID, upd) + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(ret) + } } From 228f0ff86de20a6540f6c057aaae3633388ec76e Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Thu, 30 May 2024 06:35:57 -0500 Subject: [PATCH 08/17] Started work on crash reporting --- internal/darkstorm_backend/README.md | 2 + internal/darkstorm_backend/crash.go | 76 +++++++++++++++++----------- internal/darkstorm_backend/db.go | 8 ++- internal/darkstorm_backend/user.go | 6 +++ 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 4239662..799c43b 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -105,6 +105,8 @@ If an error status code is returned then the body will be as follows. `errorCode`'s returned from the main library: +* misconfigured + * Backend is configured incorrectly (such as App returning nil crash table, but key has crash permission) * invalidKey * API Key is invalid or does not have the needed permission for the request. * invalidBody diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 5a1dade..064ffb3 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -1,59 +1,77 @@ package darkstorm -import "net/http" +import ( + "encoding/json" + "errors" + "net/http" +) type ArchivedCrash struct { - Error string - Stack string - Platform string + Error string `json:"error" bson:"error"` + Stack string `json:"stack" bson:"stack"` + Platform string `json:"platform" bson:"platform"` } type IndividualCrash struct { - Platform string - Error string - Stack string - Count int + Platform string `json:"platform" bson:"platform"` + Error string `json:"error" bson:"error"` + Stack string `json:"stack" bson:"stack"` + Count int `json:"count" bson:"count"` } type CrashReport struct { - ID string - Error string - FirstLine string - Individual []IndividualCrash + ID string `json:"id" bson:"_id"` + Error string `json:"error" bson:"error"` + FirstLine string `json:"firstLine" bson:"firstLine"` + Individual []IndividualCrash `json:"individual" bson:"individual"` } func (c CrashReport) GetID() string { return c.ID } -type crashReq struct { - ID string - Platform string - Error string - Stack string -} - func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { + var ap App hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["crash"] { - w.WriteHeader(http.StatusUnauthorized) + if hdr.k != nil { + ap = b.GetApp(hdr.k) + } + if ap == nil || hdr.k.Perm["crash"] || errors.Is(err, ErrApiKeyUnauthorized) { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return } - if err != nil { - //TODO + defer r.Body.Close() + var crash IndividualCrash + err = json.NewDecoder(r.Body).Decode(&crash) + if err != nil || crash.Platform == "" || crash.Error == "" || crash.Stack == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") return } - //TODO + tab := ap.CrashTable() + if tab == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server misconfigured") + return + } + if !tab.IsArchived(crash) { + err = tab.InsertCrash(crash) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + } + w.WriteHeader(http.StatusCreated) } func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["management"] { - w.WriteHeader(http.StatusUnauthorized) + if hdr.k == nil || hdr.k.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return - } - if err != nil { - //TODO + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return } //TODO diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index 0a9e9ac..94e17f8 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -21,5 +21,11 @@ type Table[T IDStruct] interface { type CrashTable interface { Table[CrashReport] - InsertCrash(ID string, report IndividualCrash) error + // Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. + // If a CrashReport ends up with an empty Individual array it should also be deleted. + Archive(ArchivedCrash) + IsArchived(IndividualCrash) bool + // Add the IndividualCrash report to the crash table. If a CrashReport exists that matches, then it gets added to CrashReport.Individual. + // If an IndividualCrash exists that is a perfect match, Count is incremented instead of adding it to the array. + InsertCrash(IndividualCrash) error } diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index 5b5699a..e97345a 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -118,6 +118,9 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return } defer r.Body.Close() var req createUserRequest @@ -186,6 +189,9 @@ func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return } defer r.Body.Close() var req loginRequest From 2040631737fc2574ae0f8def77411af75b3acde9 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 31 May 2024 06:59:30 -0500 Subject: [PATCH 09/17] Added cleanup loop Added LogTable interface --- internal/darkstorm_backend/README.md | 2 +- internal/darkstorm_backend/app.go | 2 +- internal/darkstorm_backend/crash.go | 29 +++++++++++++++++ internal/darkstorm_backend/darkstorm.go | 41 ++++++++++++++++--------- internal/darkstorm_backend/db.go | 6 ++++ 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 799c43b..90ad894 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -29,7 +29,7 @@ The special appID "darkstormManagement" is used to manage all apps. { id: "UUID", platform: "android", - Date: 20240519 // YYYYMMD + Date: 20240519 // YYYYMMDD as int } ``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index dd33d16..8a9545a 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -5,7 +5,7 @@ import "net/http" // An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden. type App interface { AppID() string - LogTable() Table[Log] + LogTable() LogTable CrashTable() CrashTable } diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 064ffb3..3b4a4a6 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -77,6 +77,20 @@ func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { //TODO } +func (b *Backend) managementDeleteCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + //TODO +} + +func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID string) {} + func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) if hdr.k == nil || hdr.k.Perm["management"] { @@ -89,3 +103,18 @@ func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { } //TODO } + +func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) { + hdr, err := b.ParseHeader(r) + if hdr.k == nil || hdr.k.Perm["management"] { + w.WriteHeader(http.StatusUnauthorized) + return + } + if err != nil { + //TODO + return + } + //TODO +} + +func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive ArchivedCrash) {} diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index d93766a..76b0593 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -10,13 +10,14 @@ import ( ) type Backend struct { - userTable Table[User] - keyTable Table[ApiKey] - m *http.ServeMux - apps map[string]App - jwtPriv ed25519.PrivateKey - jwtPub ed25519.PublicKey - userMutex sync.RWMutex + userTable Table[User] + keyTable Table[ApiKey] + m *http.ServeMux + apps map[string]App + managementKeyID string + jwtPriv ed25519.PrivateKey + jwtPub ed25519.PublicKey + userMutex sync.RWMutex } func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { @@ -50,19 +51,29 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { b.m.HandleFunc("POST /crash", b.reportCrash) b.m.HandleFunc("DELETE /crash/{crashID}", b.deleteCrash) b.m.HandleFunc("POST /crash/archive", b.archiveCrash) - b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.deleteCrash) - b.m.HandleFunc("POST /{appID}/crash/archive", b.archiveCrash) } - b.startCleanupLoop() + go b.cleanupLoop() return b, nil } -func (b *Backend) startCleanupLoop() { - go func() { - for range time.Tick(24 * time.Hour) { - //TODO +func (b *Backend) cleanupLoop() { + for range time.Tick(24 * time.Hour) { + oldTim := time.Now().Add(-30 * 24 * time.Hour) + old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day() + for _, a := range b.apps { + tab := a.LogTable() + if tab == nil { + continue + } + tab.RemoveOldLogs(old) } - }() + } +} + +func (b *Backend) EnableManagementKey(managementID string) { + b.managementKeyID = managementID + b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash) + b.m.HandleFunc("POST /{appID}/crash/archive", b.managementArchiveCrash) } func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index 94e17f8..fd69475 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -19,6 +19,12 @@ type Table[T IDStruct] interface { PartUpdate(ID string, update map[string]any) error } +type LogTable interface { + Table[Log] + // Remove all Log items that have a Log.Date value less then the given value. + RemoveOldLogs(date int) +} + type CrashTable interface { Table[CrashReport] // Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. From 99c881b51e9d718a8abd0323fd247abd1e40bad1 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Fri, 7 Jun 2024 11:10:46 -0500 Subject: [PATCH 10/17] Filter out some basic errors in ParseHeader Added VerifyHeader to reduce repeated code (that's everywhere) Fixed ParsedHeader values not being exported. --- internal/darkstorm_backend/crash.go | 27 +++++------ internal/darkstorm_backend/header.go | 72 ++++++++++++++++++++++------ internal/darkstorm_backend/user.go | 14 +++--- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 3b4a4a6..21f75e7 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -3,6 +3,7 @@ package darkstorm import ( "encoding/json" "errors" + "log" "net/http" ) @@ -31,18 +32,14 @@ func (c CrashReport) GetID() string { } func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { - var ap App - hdr, err := b.ParseHeader(r) - if hdr.k != nil { - ap = b.GetApp(hdr.k) - } - if ap == nil || hdr.k.Perm["crash"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "crash", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } + ap := b.GetApp(hdr.Key) defer r.Body.Close() var crash IndividualCrash err = json.NewDecoder(r.Body).Decode(&crash) @@ -52,12 +49,14 @@ func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { } tab := ap.CrashTable() if tab == nil { + log.Printf("key %v has crash permission, but app does not have a crash table", hdr.Key.AppID) ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server misconfigured") return } if !tab.IsArchived(crash) { err = tab.InsertCrash(crash) if err != nil { + log.Println("crash insertion error:", err) ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return } @@ -67,7 +66,7 @@ func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { + if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return } else if err != nil { @@ -79,7 +78,7 @@ func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { func (b *Backend) managementDeleteCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { + if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return } else if err != nil { @@ -93,7 +92,7 @@ func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID strin func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["management"] { + if hdr.Key == nil || hdr.Key.Perm["management"] { w.WriteHeader(http.StatusUnauthorized) return } @@ -106,7 +105,7 @@ func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || hdr.k.Perm["management"] { + if hdr.Key == nil || hdr.Key.Perm["management"] { w.WriteHeader(http.StatusUnauthorized) return } diff --git a/internal/darkstorm_backend/header.go b/internal/darkstorm_backend/header.go index 60d4b8a..ead4d78 100644 --- a/internal/darkstorm_backend/header.go +++ b/internal/darkstorm_backend/header.go @@ -15,31 +15,33 @@ var ( ) type ParsedHeader struct { - u *ReqUser - k *ApiKey + User *ReqUser + Key *ApiKey } -// Parses the X-API-Key and Authorization headers. If the API Key provided but invalid (either due to expiring or isn't found), -// ErrApiKeyUnauthorized is part of the returned error (check with errors.Is). +// Parses the X-API-Key and Authorization headers. If the API Key provided but invalid (either due to expiring or isn't found), ErrApiKeyUnauthorized is returned. // If the Authorization header is present but invalid, ErrTokenUnauthorized is part of the returned error (check with errors.Is). -func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { - out := ParsedHeader{} +// NOTE: An invalid apiKey will cause a nil return, but a invalid token will not. Token parsing is only +func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error) { + out := &ParsedHeader{} key := r.Header.Get("X-API-Key") token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") if key != "" { apiKey, err := b.keyTable.Get(key) - if err != nil { - return out, errors.Join(ErrApiKeyUnauthorized, err) + if err == ErrNotFound { + return nil, ErrApiKeyUnauthorized + } else if err != nil { + return nil, err } if apiKey.Death > 0 && time.Unix(apiKey.Death, 0).Before(time.Now()) { - return out, ErrApiKeyUnauthorized + return nil, ErrApiKeyUnauthorized } - out.k = &apiKey + out.Key = &apiKey } if token != "" && b.userTable != nil { t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { return b.jwtPub, nil - }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired()) + }, jwt.WithIssuer("darkstorm.tech"), jwt.WithExpirationRequired(), jwt.WithValidMethods([]string{"EdDSA"})) if err != nil { return out, errors.Join(ErrTokenUnauthorized, err) } @@ -48,21 +50,61 @@ func (b *Backend) ParseHeader(r *http.Request) (ParsedHeader, error) { return out, ErrTokenUnauthorized } sub, err := t.Claims.GetSubject() - if err != nil { + if err == jwt.ErrInvalidKey { + return out, ErrTokenUnauthorized + } else if err != nil { return out, errors.Join(ErrTokenUnauthorized, err) } usr, err := b.userTable.Get(sub) - if err != nil { + if err == jwt.ErrInvalidKey { + return out, ErrTokenUnauthorized + } else if err != nil { return out, errors.Join(ErrTokenUnauthorized, err) } iss, err := t.Claims.GetIssuedAt() - if err != nil { + if err == jwt.ErrInvalidKey { + return out, ErrTokenUnauthorized + } else if err != nil { return out, errors.Join(ErrTokenUnauthorized, err) } if usr.PasswordChange > 0 && iss.Time.Before(time.Unix(usr.PasswordChange, 0)) { return out, ErrTokenUnauthorized } - out.u = usr.toReqUser() + out.User = usr.toReqUser() } return out, nil } + +// Similiar to ParseHeader, but with key checking and automatic error returns. Guarentess Backend.GetApp is non-nil +// Checks that the key is a management key (not management permission and if allowManagement is true) or that it has the necessary permission. +// If the check if failed, ReturnError will be called and the returned *ParsedHeader will be nil. +// If token is present but invalid, no error will be returned just ParsedHeader.User will be nil. +// The error return will only be populated on "internal" errors and should *probably* be logged. +func (b *Backend) VerifyHeader(w http.ResponseWriter, r *http.Request, keyPerm string, allowManagementKey bool) (*ParsedHeader, error) { + hdr, err := b.ParseHeader(r) + if hdr == nil || hdr.Key == nil { + if err != ErrApiKeyUnauthorized { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return nil, nil + } + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return nil, err + } + if err != nil && !errors.Is(err, ErrTokenUnauthorized) { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return nil, err + } + if hdr.Key.AppID == b.managementKeyID { + if allowManagementKey { + return hdr, nil + } else { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return nil, nil + } + } + if _, ok := b.apps[hdr.Key.AppID]; !ok { + ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return nil, errors.New("server misconfigured, appID present in DB, but App not added to backend") + } + return hdr, nil +} diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index e97345a..e28e0ef 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "log" "net/http" "time" @@ -114,12 +115,11 @@ type createUserReturn struct { } func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "user", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } defer r.Body.Close() @@ -186,7 +186,7 @@ type loginReturn struct { func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { hdr, err := b.ParseHeader(r) - if hdr.k == nil || !hdr.k.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { + if hdr.Key == nil || !hdr.Key.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") return } else if err != nil { From df3fe83c5f7a48a5d06cac1e693875afae31dd34 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Tue, 11 Jun 2024 13:42:58 -0500 Subject: [PATCH 11/17] Finished with crash requests Change Log to Count Added option to get user count Moved functions to VerifyHeader Added user delete --- internal/darkstorm_backend/README.md | 76 +++++++++++++---- internal/darkstorm_backend/app.go | 2 +- internal/darkstorm_backend/crash.go | 107 +++++++++++++++++------- internal/darkstorm_backend/darkstorm.go | 11 ++- internal/darkstorm_backend/db.go | 9 +- internal/darkstorm_backend/header.go | 2 +- internal/darkstorm_backend/log.go | 41 +++++++-- internal/darkstorm_backend/user.go | 77 +++++++++++------ 8 files changed, 241 insertions(+), 84 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 90ad894..416026c 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -6,8 +6,6 @@ This is a purposefully "simple" application backend made specifically for _my_ a ### API Key -The special appID "darkstormManagement" is used to manage all apps. - ```json { id: "API Key", @@ -15,7 +13,7 @@ The special appID "darkstormManagement" is used to manage all apps. death: -1, // unix timestamp (seconds) when the key is no longer valid. -1 means there is not expected expiration (that can change in the future) perm: { user: true, // create and login users - log: true, // log users + count: true, // count users crash: true, // crash reports management: false, // managing // further permissions can be added as needed @@ -23,7 +21,9 @@ The special appID "darkstormManagement" is used to manage all apps. } ``` -### DB Log +Optionally you can set a special AppID to be a management key. Setting a management key enables management requests. + +### Count log ```json { @@ -111,16 +111,42 @@ If an error status code is returned then the body will be as follows. * API Key is invalid or does not have the needed permission for the request. * invalidBody * Body of the request is malformed. +* badRequest + * Some part of your request is invalid * internal * Server-side issue. -### Log +### Count -API Key must have the `log` permission. +API Key must have the `Count` permission. Request: -> POST: /log/{uuid} +> POST: /count/{uuid} + +### User Count + +Get a count of users. + +API Key must have the `management` permission. + +`platform` query is optional (defaults to all). + +Request: + +> GET: /count?platform=all + +With management key: + +> GET: /{appID}/count?platform=all + +Returns: + +```json +{ + count: 0 +} +``` ### Users @@ -128,6 +154,8 @@ Request: All requsests pertaining to users requires the `X-API-Key` header and the key must have the `users` permission. +Enabled by using `Backend.AddUserAuth`. + #### Create User > TODO: Email user to confirm. @@ -164,11 +192,19 @@ If returned status is 401, the errorCode will be one of the following: * password * Password is to short or too long. +#### Delete User + +Requires either the `management` permission or a management key. + +Request: + +> DELETE: /user/{userID} + #### Login Request: -> POST: /user +> POST: /user/login ```json { @@ -196,9 +232,21 @@ Possible `error` values: * invalid * Either the username or password is incorrect -### Crash Report +#### Change Password -> TODO: Archive a crash to prevent it being reported again. +Request: + +> POST: /user/changepassword + +```json +{ + token: "JWT Token", + old: "Old Password", + new: "New Password" +} +``` + +### Crash Report #### Report @@ -227,7 +275,7 @@ Request: > DELETE: /crash/{crashID} -With "darkstormManagement" key: +With management key: > DELETE: /{appID}/crash/{crashID} @@ -239,16 +287,16 @@ Request: > POST: /crash/archive -With "darkstormManagement" key: +With management key: -> POST: /{appID}/crash/{crashID} +> POST: /{appID}/crash/archive Request Body: ```json { error: "error", - stack: "full stacktrace", + stack: "full stacktrace", // Archives will only match against a perfect match. platform: "all", // Limit the archive to a specific platform, or use "all". } ``` diff --git a/internal/darkstorm_backend/app.go b/internal/darkstorm_backend/app.go index 8a9545a..6b3d812 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/darkstorm_backend/app.go @@ -5,7 +5,7 @@ import "net/http" // An application interface. Both LogTable and CrashTable are optional, if they return nil then requests will be forbidden. type App interface { AppID() string - LogTable() LogTable + CountTable() CountTable CrashTable() CrashTable } diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 21f75e7..6748a27 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -2,7 +2,6 @@ package darkstorm import ( "encoding/json" - "errors" "log" "net/http" ) @@ -65,55 +64,105 @@ func (b *Backend) reportCrash(w http.ResponseWriter, r *http.Request) { } func (b *Backend) deleteCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "management", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - //TODO + crashID := r.PathValue("crashID") + if crashID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + b.actualCrashDelete(w, b.GetApp(hdr.Key), crashID) } func (b *Backend) managementDeleteCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - //TODO + crashID := r.PathValue("crashID") + if crashID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + appID := r.PathValue("appID") + ap := b.apps[appID] + if ap == nil || appID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + b.actualCrashDelete(w, ap, crashID) } -func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID string) {} +func (b *Backend) actualCrashDelete(w http.ResponseWriter, ap App, crashID string) { + crash := ap.CrashTable() + if crash == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server Misconfigured") + return + } + err := crash.Remove(crashID) + if err != nil && err != ErrNotFound { + log.Println("error when deleting crash:", err) + } +} func (b *Backend) archiveCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] { - w.WriteHeader(http.StatusUnauthorized) + hdr, err := b.VerifyHeader(w, r, "management", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - if err != nil { - //TODO + defer r.Body.Close() + var toArchive ArchivedCrash + err = json.NewDecoder(r.Body).Decode(&toArchive) + if err != nil || toArchive.Platform == "" || toArchive.Error == "" || toArchive.Stack == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") return } - //TODO + b.actualCrashArchive(w, b.GetApp(hdr.Key), toArchive) } func (b *Backend) managementArchiveCrash(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || hdr.Key.Perm["management"] { - w.WriteHeader(http.StatusUnauthorized) + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } - if err != nil { - //TODO + appID := r.PathValue("appID") + ap := b.apps[appID] + if ap == nil || appID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") return } - //TODO + defer r.Body.Close() + var toArchive ArchivedCrash + err = json.NewDecoder(r.Body).Decode(&toArchive) + if err != nil || toArchive.Platform == "" || toArchive.Error == "" || toArchive.Stack == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") + return + } + b.actualCrashArchive(w, ap, toArchive) } -func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive ArchivedCrash) {} +func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive ArchivedCrash) { + crash := ap.CrashTable() + if crash == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server Misconfigured") + return + } + err := crash.Archive(toArchive) + if err != nil { + log.Println() + } +} diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 76b0593..8feee4b 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -37,7 +37,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { if ext, is := apps[i].(ExtendedApp); is { b.m.HandleFunc("/"+apps[i].AppID()+"/", ext.Extension) } - if !hasLog && apps[i].LogTable() != nil { + if !hasLog && apps[i].CountTable() != nil { hasLog = true } if !hasCrash && apps[i].CrashTable() != nil { @@ -45,7 +45,8 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { } } if hasLog { - b.m.HandleFunc("POST /log/{uuid}", b.log) + b.m.HandleFunc("POST /count/{uuid}", b.countLog) + b.m.HandleFunc("GET /count", b.getCount) } if hasCrash { b.m.HandleFunc("POST /crash", b.reportCrash) @@ -61,7 +62,7 @@ func (b *Backend) cleanupLoop() { oldTim := time.Now().Add(-30 * 24 * time.Hour) old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day() for _, a := range b.apps { - tab := a.LogTable() + tab := a.CountTable() if tab == nil { continue } @@ -74,12 +75,16 @@ func (b *Backend) EnableManagementKey(managementID string) { b.managementKeyID = managementID b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash) b.m.HandleFunc("POST /{appID}/crash/archive", b.managementArchiveCrash) + b.m.HandleFunc("GET /{appID}/count", b.getCount) } func (b *Backend) AddUserAuth(userTable Table[User], privKey, pubKey []byte) { b.userTable = userTable b.jwtPriv = privKey b.jwtPub = pubKey + b.m.HandleFunc("POST /user/create", b.createUser) + b.m.HandleFunc("DELETE /user/{userID}", b.deleteUser) + b.m.HandleFunc("POST /user/login", b.login) } func (b *Backend) HandleFunc(pattern string, h http.HandlerFunc) { diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index fd69475..f58b8a1 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -19,17 +19,18 @@ type Table[T IDStruct] interface { PartUpdate(ID string, update map[string]any) error } -type LogTable interface { - Table[Log] - // Remove all Log items that have a Log.Date value less then the given value. +type CountTable interface { + Table[CountLog] + // Remove all Log items that have a CountLog.Date value less then the given value. RemoveOldLogs(date int) + Count(platform string) int } type CrashTable interface { Table[CrashReport] // Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. // If a CrashReport ends up with an empty Individual array it should also be deleted. - Archive(ArchivedCrash) + Archive(ArchivedCrash) error IsArchived(IndividualCrash) bool // Add the IndividualCrash report to the crash table. If a CrashReport exists that matches, then it gets added to CrashReport.Individual. // If an IndividualCrash exists that is a perfect match, Count is incremented instead of adding it to the array. diff --git a/internal/darkstorm_backend/header.go b/internal/darkstorm_backend/header.go index ead4d78..cfcbe0c 100644 --- a/internal/darkstorm_backend/header.go +++ b/internal/darkstorm_backend/header.go @@ -15,7 +15,7 @@ var ( ) type ParsedHeader struct { - User *ReqUser + User *ReqestUser Key *ApiKey } diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go index 309fb48..bf4cb0a 100644 --- a/internal/darkstorm_backend/log.go +++ b/internal/darkstorm_backend/log.go @@ -1,17 +1,48 @@ package darkstorm -import "net/http" +import ( + "log" + "net/http" +) -type Log struct { +type CountLog struct { ID string Platform string Date int } -func (l Log) GetID() string { - return l.ID +func (c CountLog) GetID() string { + return c.ID } -func (b *Backend) log(w http.ResponseWriter, r *http.Request) { +func (b *Backend) countLog(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "count", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + //TODO +} + +func (b *Backend) getCount(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + var ap App + if hdr.Key.AppID == b.managementKeyID { + ap = b.apps[r.PathValue("appID")] + if ap == nil { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad request") + return + } + } else { + ap = b.GetApp(hdr.Key) + } //TODO } diff --git a/internal/darkstorm_backend/user.go b/internal/darkstorm_backend/user.go index e28e0ef..4555326 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/darkstorm_backend/user.go @@ -24,6 +24,24 @@ func generateSalt() (string, error) { return base64.RawStdEncoding.EncodeToString(out), err } +type ReqestUser struct { + Perm map[string]string + ID string + Username string +} + +func (b *Backend) GenerateJWT(r *ReqestUser) (string, error) { + if b.jwtPriv == nil || b.jwtPub == nil { + return "", errors.New("user management not enabled") + } + return jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{ + ID: r.ID, + Issuer: "darkstorm.tech", + IssuedAt: jwt.NewNumericDate(time.Now()), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * time.Hour)), + }).SignedString(b.jwtPriv) +} + type User struct { Perm map[string]string `json:"perm" bson:"perm"` ID string `json:"id" bson:"_id"` @@ -59,27 +77,12 @@ func NewUser(username, password, email string) (User, error) { return u, nil } -type ReqUser struct { - Perm map[string]string - ID string - Username string -} - -func (b *Backend) generateJWT(r *ReqUser) (string, error) { - return jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.RegisteredClaims{ - ID: r.ID, - Issuer: "darkstorm.tech", - IssuedAt: jwt.NewNumericDate(time.Now()), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(12 * time.Hour)), - }).SignedString(b.jwtPriv) -} - func (u User) GetID() string { return u.ID } -func (u User) toReqUser() *ReqUser { - return &ReqUser{ +func (u User) toReqUser() *ReqestUser { + return &ReqestUser{ Perm: u.Perm, ID: u.ID, Username: u.Username, @@ -114,7 +117,7 @@ type createUserReturn struct { Token string `json:"token"` } -func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { +func (b *Backend) createUser(w http.ResponseWriter, r *http.Request) { hdr, err := b.VerifyHeader(w, r, "user", false) if hdr == nil { if err == nil { @@ -164,7 +167,7 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { } var ret createUserReturn ret.Username = u.Username - ret.Token, err = b.generateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.toReqUser()) if err != nil { ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return @@ -173,6 +176,27 @@ func (b *Backend) CreateUser(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ret) } +func (b *Backend) deleteUser(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "management", true) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } + return + } + userID := r.PathValue("userID") + if userID == "" { + ReturnError(w, http.StatusBadRequest, "badRequest", "Bad Request") + return + } + b.userMutex.Lock() + defer b.userMutex.Unlock() + err = b.userTable.Remove(userID) + if err != nil && err != ErrNotFound { + log.Println("error deleting user:", err) + } +} + type loginRequest struct { Username string Password string @@ -184,13 +208,12 @@ type loginReturn struct { Timeout int64 `json:"timeout"` } -func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { - hdr, err := b.ParseHeader(r) - if hdr.Key == nil || !hdr.Key.Perm["user"] || errors.Is(err, ErrApiKeyUnauthorized) { - ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") - return - } else if err != nil { - ReturnError(w, http.StatusInternalServerError, "internal", "Server error") +func (b *Backend) login(w http.ResponseWriter, r *http.Request) { + hdr, err := b.VerifyHeader(w, r, "user", false) + if hdr == nil { + if err == nil { + log.Println("request key parsing error:", err) + } return } defer r.Body.Close() @@ -224,7 +247,7 @@ func (b *Backend) Login(w http.ResponseWriter, r *http.Request) { return } if u.Password == hash { - ret.Token, err = b.generateJWT(u.toReqUser()) + ret.Token, err = b.GenerateJWT(u.toReqUser()) if err != nil { ReturnError(w, http.StatusInternalServerError, "internal", "Server error") return From e3af23873f7300efb128057f229f04a9f369e1a0 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Wed, 12 Jun 2024 02:17:26 -0500 Subject: [PATCH 12/17] Added counting Changed Count post URL Archiving crashes now removes crashes that match the archive --- internal/darkstorm_backend/README.md | 19 ++++- internal/darkstorm_backend/crash.go | 35 +++++++- internal/darkstorm_backend/darkstorm.go | 9 +- internal/darkstorm_backend/darkstorm_test.go | 19 ++++- internal/darkstorm_backend/db.go | 4 +- internal/darkstorm_backend/log.go | 87 ++++++++++++++++++-- 6 files changed, 157 insertions(+), 16 deletions(-) diff --git a/internal/darkstorm_backend/README.md b/internal/darkstorm_backend/README.md index 416026c..7ccb267 100644 --- a/internal/darkstorm_backend/README.md +++ b/internal/darkstorm_backend/README.md @@ -118,11 +118,26 @@ If an error status code is returned then the body will be as follows. ### Count -API Key must have the `Count` permission. +API Key must have the `count` permission. Request: -> POST: /count/{uuid} +> POST: /count + +```json +{ + id: "uuid", // Should be an empty string on first request. If invalid or too old, a new UUID will be returned. + platform: "web" +} +``` + +Returns: + +```json +{ + id: "uuid" +} +``` ### User Count diff --git a/internal/darkstorm_backend/crash.go b/internal/darkstorm_backend/crash.go index 6748a27..73b660e 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/darkstorm_backend/crash.go @@ -4,6 +4,7 @@ import ( "encoding/json" "log" "net/http" + "strings" ) type ArchivedCrash struct { @@ -163,6 +164,38 @@ func (b *Backend) actualCrashArchive(w http.ResponseWriter, ap App, toArchive Ar } err := crash.Archive(toArchive) if err != nil { - log.Println() + log.Println("error archive crash:", err) + return + } + first, _, _ := strings.Cut(toArchive.Stack, "\n") + crashes, err := crash.Find(map[string]any{"error": toArchive.Error, "firstLine": first}) + if err == ErrNotFound { + return + } else if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + for _, c := range crashes { + ogLen := len(c.Individual) + for i := 0; i < len(c.Individual); i++ { + ind := c.Individual[i] + if ind.Stack == toArchive.Stack { + if toArchive.Platform == "all" || toArchive.Platform == ind.Platform { + c.Individual = append(c.Individual[:i], c.Individual[i+1:]...) + i-- + } + } + } + if len(c.Individual) == 0 { + err = crash.Remove(c.ID) + if err != nil { + log.Println("error removing empty crash report:", err) + } + } else if len(c.Individual) < ogLen { + err = crash.PartUpdate(c.ID, map[string]any{"individual": c.Individual}) + if err != nil { + log.Println("error updating individual crash reports:", err) + } + } } } diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/darkstorm_backend/darkstorm.go index 8feee4b..06e5d05 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/darkstorm_backend/darkstorm.go @@ -45,7 +45,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { } } if hasLog { - b.m.HandleFunc("POST /count/{uuid}", b.countLog) + b.m.HandleFunc("POST /count", b.countLog) b.m.HandleFunc("GET /count", b.getCount) } if hasCrash { @@ -59,8 +59,7 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { func (b *Backend) cleanupLoop() { for range time.Tick(24 * time.Hour) { - oldTim := time.Now().Add(-30 * 24 * time.Hour) - old := (oldTim.Year() * 10000) + (int(oldTim.Month()) * 100) + oldTim.Day() + old := getDate(time.Now().Add(-30 * 24 * time.Hour)) for _, a := range b.apps { tab := a.CountTable() if tab == nil { @@ -71,6 +70,10 @@ func (b *Backend) cleanupLoop() { } } +func getDate(t time.Time) int { + return (t.Year() * 10000) + (int(t.Month()) * 100) + t.Day() +} + func (b *Backend) EnableManagementKey(managementID string) { b.managementKeyID = managementID b.m.HandleFunc("DELETE /{appID}/crash/{crashID}", b.managementDeleteCrash) diff --git a/internal/darkstorm_backend/darkstorm_test.go b/internal/darkstorm_backend/darkstorm_test.go index 53758c2..fd1346e 100644 --- a/internal/darkstorm_backend/darkstorm_test.go +++ b/internal/darkstorm_backend/darkstorm_test.go @@ -1,10 +1,23 @@ package darkstorm import ( - "os" + "fmt" "testing" + "time" + + "github.com/google/uuid" ) -func TestMain(t *testing.M) { - os.Exit(t.Run()) +func TestStuff(t *testing.T) { + for i := 0; i < 50; i++ { + go func() { + id, err := uuid.NewV7() + if err != nil { + fmt.Println(err) + } + fmt.Println(id.String()) + }() + } + time.Sleep(3 * time.Second) + t.Fatal("end") } diff --git a/internal/darkstorm_backend/db.go b/internal/darkstorm_backend/db.go index f58b8a1..189f993 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/darkstorm_backend/db.go @@ -23,13 +23,13 @@ type CountTable interface { Table[CountLog] // Remove all Log items that have a CountLog.Date value less then the given value. RemoveOldLogs(date int) + // Get count. If platform is an empty string or "all", the full count should be given Count(platform string) int } type CrashTable interface { Table[CrashReport] - // Move a crash type to archive. All instances that perfectly match that appear in CrashReport.Individual should be deleted. - // If a CrashReport ends up with an empty Individual array it should also be deleted. + // Move a crash type to archive. Crashes that match the archived crash will be automatically removed from the CrashTable. Archive(ArchivedCrash) error IsArchived(IndividualCrash) bool // Add the IndividualCrash report to the crash table. If a CrashReport exists that matches, then it gets added to CrashReport.Individual. diff --git a/internal/darkstorm_backend/log.go b/internal/darkstorm_backend/log.go index bf4cb0a..0d6972d 100644 --- a/internal/darkstorm_backend/log.go +++ b/internal/darkstorm_backend/log.go @@ -1,20 +1,29 @@ package darkstorm import ( + "encoding/json" "log" "net/http" + "time" + + "github.com/google/uuid" ) type CountLog struct { - ID string - Platform string - Date int + ID string `json:"id" bson:"_id"` + Platform string `json:"platform" bson:"platform"` + Date int `json:"date" bson:"date"` } func (c CountLog) GetID() string { return c.ID } +type countLogReq struct { + ID string + Platform string +} + func (b *Backend) countLog(w http.ResponseWriter, r *http.Request) { hdr, err := b.VerifyHeader(w, r, "count", false) if hdr == nil { @@ -23,7 +32,69 @@ func (b *Backend) countLog(w http.ResponseWriter, r *http.Request) { } return } - //TODO + defer r.Body.Close() + var req countLogReq + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil || req.Platform == "" { + ReturnError(w, http.StatusBadRequest, "invalidBody", "Bad request") + return + } + ap := b.GetApp(hdr.Key) + count := ap.CountTable() + if count == nil { + ReturnError(w, http.StatusInternalServerError, "misconfigured", "Server Misconfigured") + return + } + curDate := getDate(time.Now()) + if req.ID == "" { + err = addToCountTable(w, count, req.Platform, curDate) + if err != nil { + log.Println("error adding to count table:", err) + } + return + } + l, err := count.Get(req.ID) + if err == ErrNotFound { + err = addToCountTable(w, count, req.Platform, curDate) + if err != nil { + log.Println("error adding to count table:", err) + } + return + } + if l.Date >= curDate { + json.NewEncoder(w).Encode(map[string]string{"id": req.ID}) + w.WriteHeader(http.StatusCreated) + return + } + err = count.PartUpdate(req.ID, map[string]any{"date": curDate}) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + json.NewEncoder(w).Encode(map[string]string{"id": req.ID}) + w.WriteHeader(http.StatusCreated) +} + +func addToCountTable(w http.ResponseWriter, c CountTable, platform string, curDate int) error { + id, err := uuid.NewV7() + if err != nil { + log.Println("error generating new log UUID:", err) + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return err + } + err = c.Insert(CountLog{ + ID: id.String(), + Platform: platform, + Date: curDate, + }) + if err != nil { + log.Println("error inserting new count log:", err) + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return err + } + json.NewEncoder(w).Encode(map[string]string{"id": id.String()}) + w.WriteHeader(http.StatusCreated) + return nil } func (b *Backend) getCount(w http.ResponseWriter, r *http.Request) { @@ -44,5 +115,11 @@ func (b *Backend) getCount(w http.ResponseWriter, r *http.Request) { } else { ap = b.GetApp(hdr.Key) } - //TODO + count := ap.CountTable() + if count == nil { + ReturnError(w, http.StatusBadRequest, "badRequest", "Trying to get user count on app that doesn't have a count table") + return + } + out := count.Count(r.URL.Query().Get("platform")) + json.NewEncoder(w).Encode(map[string]int{"count": out}) } From 28654e237e3c2df693c96aa11c404ae745c6e5c1 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Wed, 12 Jun 2024 03:31:09 -0500 Subject: [PATCH 13/17] Move darkstorm_backend to just backend Added MongoDB instances of DB tables Updated some DB interfaces Added logging to count cleanup --- go.mod | 16 +++- go.sum | 58 ++++++++++++- .../{darkstorm_backend => backend}/README.md | 0 .../{darkstorm_backend => backend}/app.go | 2 +- .../{darkstorm_backend => backend}/crash.go | 2 +- .../darkstorm.go | 10 ++- .../darkstorm_test.go | 2 +- internal/{darkstorm_backend => backend}/db.go | 8 +- internal/backend/db/mongo.go | 83 +++++++++++++++++++ internal/backend/db/mongo_crash.go | 70 ++++++++++++++++ internal/backend/db/valkey.go | 1 + .../{darkstorm_backend => backend}/header.go | 4 +- .../{darkstorm_backend => backend}/key.go | 2 +- .../{darkstorm_backend => backend}/log.go | 8 +- .../{darkstorm_backend => backend}/user.go | 2 +- 15 files changed, 246 insertions(+), 22 deletions(-) rename internal/{darkstorm_backend => backend}/README.md (100%) rename internal/{darkstorm_backend => backend}/app.go (95%) rename internal/{darkstorm_backend => backend}/crash.go (99%) rename internal/{darkstorm_backend => backend}/darkstorm.go (92%) rename internal/{darkstorm_backend => backend}/darkstorm_test.go (94%) rename internal/{darkstorm_backend => backend}/db.go (89%) create mode 100644 internal/backend/db/mongo.go create mode 100644 internal/backend/db/mongo_crash.go create mode 100644 internal/backend/db/valkey.go rename internal/{darkstorm_backend => backend}/header.go (99%) rename internal/{darkstorm_backend => backend}/key.go (88%) rename internal/{darkstorm_backend => backend}/log.go (94%) rename internal/{darkstorm_backend => backend}/user.go (99%) diff --git a/go.mod b/go.mod index 9131155..b115052 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,20 @@ go 1.22.3 require ( github.com/google/uuid v1.6.0 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + go.mongodb.org/mongo-driver v1.15.0 ) require ( - github.com/golang-jwt/jwt/v5 v5.2.1 - golang.org/x/sys v0.20.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index c5c1062..ea5a8fd 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,58 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/darkstorm_backend/README.md b/internal/backend/README.md similarity index 100% rename from internal/darkstorm_backend/README.md rename to internal/backend/README.md diff --git a/internal/darkstorm_backend/app.go b/internal/backend/app.go similarity index 95% rename from internal/darkstorm_backend/app.go rename to internal/backend/app.go index 6b3d812..3db5d1c 100644 --- a/internal/darkstorm_backend/app.go +++ b/internal/backend/app.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import "net/http" diff --git a/internal/darkstorm_backend/crash.go b/internal/backend/crash.go similarity index 99% rename from internal/darkstorm_backend/crash.go rename to internal/backend/crash.go index 73b660e..9ee767b 100644 --- a/internal/darkstorm_backend/crash.go +++ b/internal/backend/crash.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import ( "encoding/json" diff --git a/internal/darkstorm_backend/darkstorm.go b/internal/backend/darkstorm.go similarity index 92% rename from internal/darkstorm_backend/darkstorm.go rename to internal/backend/darkstorm.go index 06e5d05..1b9aa57 100644 --- a/internal/darkstorm_backend/darkstorm.go +++ b/internal/backend/darkstorm.go @@ -1,9 +1,10 @@ -package darkstorm +package backend import ( "crypto/ed25519" "encoding/json" "errors" + "log" "net/http" "sync" "time" @@ -60,12 +61,17 @@ func NewBackend(keyTable Table[ApiKey], apps ...App) (*Backend, error) { func (b *Backend) cleanupLoop() { for range time.Tick(24 * time.Hour) { old := getDate(time.Now().Add(-30 * 24 * time.Hour)) + var err error for _, a := range b.apps { + log.Printf("Removing logs for %v", a.AppID()) tab := a.CountTable() if tab == nil { continue } - tab.RemoveOldLogs(old) + err = tab.RemoveOldLogs(old) + if err != nil { + log.Printf("error removing old logs for %v: %v\n", a.AppID(), err) + } } } } diff --git a/internal/darkstorm_backend/darkstorm_test.go b/internal/backend/darkstorm_test.go similarity index 94% rename from internal/darkstorm_backend/darkstorm_test.go rename to internal/backend/darkstorm_test.go index fd1346e..c33a15a 100644 --- a/internal/darkstorm_backend/darkstorm_test.go +++ b/internal/backend/darkstorm_test.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import ( "fmt" diff --git a/internal/darkstorm_backend/db.go b/internal/backend/db.go similarity index 89% rename from internal/darkstorm_backend/db.go rename to internal/backend/db.go index 189f993..83b228c 100644 --- a/internal/darkstorm_backend/db.go +++ b/internal/backend/db.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import "errors" @@ -11,7 +11,7 @@ type IDStruct interface { } type Table[T IDStruct] interface { - Get(ID string) (data T, err error) + Get(ID string) (data *T, err error) Find(values map[string]any) ([]T, error) Insert(data T) error Remove(ID string) error @@ -22,9 +22,9 @@ type Table[T IDStruct] interface { type CountTable interface { Table[CountLog] // Remove all Log items that have a CountLog.Date value less then the given value. - RemoveOldLogs(date int) + RemoveOldLogs(date int) error // Get count. If platform is an empty string or "all", the full count should be given - Count(platform string) int + Count(platform string) (int, error) } type CrashTable interface { diff --git a/internal/backend/db/mongo.go b/internal/backend/db/mongo.go new file mode 100644 index 0000000..c49d718 --- /dev/null +++ b/internal/backend/db/mongo.go @@ -0,0 +1,83 @@ +package db + +import ( + "context" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type MongoTable[T backend.IDStruct] struct { + col *mongo.Collection +} + +func NewMongoTable[T backend.IDStruct](col *mongo.Collection) *MongoTable[T] { + return &MongoTable[T]{ + col: col, + } +} + +func (m *MongoTable[T]) Get(ID string) (data *T, err error) { + res := m.col.FindOne(context.Background(), bson.M{"_id": ID}) + if res.Err() == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } else if res.Err() != nil { + return nil, res.Err() + } + var out T + err = res.Decode(&out) + return &out, err +} + +func (m *MongoTable[T]) Find(values map[string]any) ([]T, error) { + res, err := m.col.Find(context.Background(), values) + if err == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } else if err != nil { + return nil, err + } + var out []T + err = res.All(context.Background(), &out) + return out, err +} + +func (m *MongoTable[T]) Insert(data T) error { + _, err := m.col.InsertOne(context.Background(), data) + return err +} + +func (m *MongoTable[T]) Remove(ID string) error { + res := m.col.FindOneAndDelete(context.Background(), bson.M{"_id": ID}) + return res.Err() +} + +func (m *MongoTable[T]) FullUpdate(ID string, data T) error { + res := m.col.FindOneAndReplace(context.Background(), bson.M{"_id": ID}, data) + if res.Err() == mongo.ErrNoDocuments { + return backend.ErrNotFound + } + return res.Err() +} + +func (m *MongoTable[T]) PartUpdate(ID string, update map[string]any) error { + res := m.col.FindOneAndUpdate(context.Background(), bson.M{"_id": ID}, update) + if res.Err() == mongo.ErrNoDocuments { + return backend.ErrNotFound + } + return res.Err() +} + +func (m *MongoTable[CountLog]) RemoveOldLogs(date int) { + m.col.DeleteMany(context.Background(), bson.M{"date": bson.M{"$lt": date}}) +} +func (m *MongoTable[CountLog]) Count(platform string) (int, error) { + var filter bson.M + if platform == "" || platform == "all" { + filter = bson.M{} + } else { + filter = bson.M{"platform": platform} + } + out, err := m.col.CountDocuments(context.Background(), filter) + return int(out), err +} diff --git a/internal/backend/db/mongo_crash.go b/internal/backend/db/mongo_crash.go new file mode 100644 index 0000000..9ee092e --- /dev/null +++ b/internal/backend/db/mongo_crash.go @@ -0,0 +1,70 @@ +package db + +import ( + "context" + "strings" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type MongoCrashTable struct { + *MongoTable[backend.CrashReport] + archiveCol *mongo.Collection +} + +func NewMongoCrashTable(crashCol *mongo.Collection, archiveCol *mongo.Collection) *MongoCrashTable { + return &MongoCrashTable{ + MongoTable: NewMongoTable[backend.CrashReport](crashCol), + archiveCol: archiveCol, + } +} + +func (m *MongoCrashTable) Archive(toArchive backend.ArchivedCrash) error { + if toArchive.Platform == "" { + toArchive.Platform = "all" + } + _, err := m.archiveCol.InsertOne(context.Background(), toArchive) + return err +} + +func (m *MongoCrashTable) IsArchived(ind backend.IndividualCrash) bool { + res := m.archiveCol.FindOne(context.Background(), + bson.M{"error": ind.Error, "stack": ind.Stack, "platform": bson.M{"$in": []string{ind.Platform, "all"}}}, + ) + return res.Err() == nil +} + +func (m *MongoCrashTable) InsertCrash(ind backend.IndividualCrash) error { + first, _, _ := strings.Cut(ind.Stack, "\n") + _, err := m.col.UpdateOne(context.Background(), + bson.M{"error": ind.Error, "firstLine": first, //filter main report + "individual.stack": ind.Stack, "individual.platform": ind.Platform}, //filter individual + bson.M{"$inc": bson.M{"individual.count": 1}}, //increment count + ) + if err == mongo.ErrNoDocuments { + ind.Count = 1 + _, err = m.col.UpdateOne(context.Background(), + bson.M{"error": ind.Error, "firstLine": first}, //filter + bson.M{"$push": bson.M{"individual": ind}}, //Add new individual report + ) + if err == mongo.ErrNoDocuments { + var id uuid.UUID + id, err = uuid.NewV7() + if err != nil { + return err + } + _, err = m.col.InsertOne(context.Background(), + backend.CrashReport{ + ID: id.String(), + Error: ind.Error, + FirstLine: first, + Individual: []backend.IndividualCrash{ind}, + }, + ) + } + } + return err +} diff --git a/internal/backend/db/valkey.go b/internal/backend/db/valkey.go new file mode 100644 index 0000000..10060e9 --- /dev/null +++ b/internal/backend/db/valkey.go @@ -0,0 +1 @@ +package db \ No newline at end of file diff --git a/internal/darkstorm_backend/header.go b/internal/backend/header.go similarity index 99% rename from internal/darkstorm_backend/header.go rename to internal/backend/header.go index cfcbe0c..09ea247 100644 --- a/internal/darkstorm_backend/header.go +++ b/internal/backend/header.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import ( "errors" @@ -36,7 +36,7 @@ func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error) { if apiKey.Death > 0 && time.Unix(apiKey.Death, 0).Before(time.Now()) { return nil, ErrApiKeyUnauthorized } - out.Key = &apiKey + out.Key = apiKey } if token != "" && b.userTable != nil { t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { diff --git a/internal/darkstorm_backend/key.go b/internal/backend/key.go similarity index 88% rename from internal/darkstorm_backend/key.go rename to internal/backend/key.go index 0fca0fd..f37a093 100644 --- a/internal/darkstorm_backend/key.go +++ b/internal/backend/key.go @@ -1,4 +1,4 @@ -package darkstorm +package backend type ApiKey struct { Perm map[string]bool diff --git a/internal/darkstorm_backend/log.go b/internal/backend/log.go similarity index 94% rename from internal/darkstorm_backend/log.go rename to internal/backend/log.go index 0d6972d..c84fa3d 100644 --- a/internal/darkstorm_backend/log.go +++ b/internal/backend/log.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import ( "encoding/json" @@ -120,6 +120,10 @@ func (b *Backend) getCount(w http.ResponseWriter, r *http.Request) { ReturnError(w, http.StatusBadRequest, "badRequest", "Trying to get user count on app that doesn't have a count table") return } - out := count.Count(r.URL.Query().Get("platform")) + out, err := count.Count(r.URL.Query().Get("platform")) + if err != nil { + ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } json.NewEncoder(w).Encode(map[string]int{"count": out}) } diff --git a/internal/darkstorm_backend/user.go b/internal/backend/user.go similarity index 99% rename from internal/darkstorm_backend/user.go rename to internal/backend/user.go index 4555326..120712c 100644 --- a/internal/darkstorm_backend/user.go +++ b/internal/backend/user.go @@ -1,4 +1,4 @@ -package darkstorm +package backend import ( "crypto/rand" From 7f9de3f025cc7ca4108904e84af28da84b3c5fa9 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Wed, 12 Jun 2024 05:18:03 -0500 Subject: [PATCH 14/17] Added json and bson tags to ApiKey --- internal/backend/key.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/backend/key.go b/internal/backend/key.go index f37a093..5d24684 100644 --- a/internal/backend/key.go +++ b/internal/backend/key.go @@ -1,10 +1,10 @@ package backend type ApiKey struct { - Perm map[string]bool - ID string - AppID string - Death int64 + Perm map[string]bool `json:"perm" bson:"perm"` + ID string `json:"id" bson:"_id" valkey:",key"` + AppID string `json:"appID" bson:"appID"` + Death int64 `json:"death" bson:"death"` } func (k ApiKey) GetID() string { From 7b15aab7ecae5122c02726d983970aa8045af926 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Sun, 16 Jun 2024 06:46:44 -0500 Subject: [PATCH 15/17] SimpleApp Started work on the actual stuff Gave up on Valkey (for right now) --- go.mod | 11 +++++++---- go.sum | 12 ++++++------ internal/backend/app.go | 24 ++++++++++++++++++++++++ internal/backend/darkstorm.go | 4 ++++ internal/backend/darkstorm_test.go | 17 +---------------- internal/backend/db/sql.go | 6 ++++++ internal/backend/db/valkey.go | 12 +++++++++++- main.go | 18 ++++++++++++++++++ 8 files changed, 77 insertions(+), 27 deletions(-) create mode 100644 internal/backend/db/sql.go diff --git a/go.mod b/go.mod index b115052..43bc30a 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,17 @@ module github.com/CalebQ42/darkstorm-server go 1.22.3 require ( - github.com/google/uuid v1.6.0 - golang.org/x/crypto v0.24.0 github.com/golang-jwt/jwt/v5 v5.2.1 - go.mongodb.org/mongo-driver v1.15.0 + github.com/google/uuid v1.6.0 + go.mongodb.org/mongo-driver v1.15.1 + golang.org/x/crypto v0.24.0 ) +require github.com/google/go-cmp v0.6.0 // indirect + require ( github.com/golang/snappy v0.0.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -18,6 +21,6 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.16.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index ea5a8fd..d4401a0 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,12 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= @@ -21,8 +23,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= -go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.mongodb.org/mongo-driver v1.15.1 h1:l+RvoUOoMXFmADTLfYDm7On9dRm7p4T80/lEQM+r7HU= +go.mongodb.org/mongo-driver v1.15.1/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= @@ -54,5 +56,3 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/backend/app.go b/internal/backend/app.go index 3db5d1c..0a3d5bf 100644 --- a/internal/backend/app.go +++ b/internal/backend/app.go @@ -14,3 +14,27 @@ type ExtendedApp interface { // Alternatively, use Backend.HandleFunc for more customizability Extension(http.ResponseWriter, *http.Request) } + +type simpleApp struct { + countTab CountTable + crashTab CrashTable + appID string +} + +func NewSimpleApp(appID string, countTable CountTable, crashTable CrashTable) App { + return &simpleApp{ + appID: appID, + countTab: countTable, + crashTab: crashTable, + } +} + +func (s *simpleApp) AppID() string { + return s.appID +} +func (s *simpleApp) CountTable() CountTable { + return s.countTab +} +func (s *simpleApp) CrashTable() CrashTable { + return s.crashTab +} diff --git a/internal/backend/darkstorm.go b/internal/backend/darkstorm.go index 1b9aa57..d5e86d8 100644 --- a/internal/backend/darkstorm.go +++ b/internal/backend/darkstorm.go @@ -76,6 +76,10 @@ func (b *Backend) cleanupLoop() { } } +func (b *Backend) ServeHTTP(w http.ResponseWriter, r *http.Request) { + b.m.ServeHTTP(w, r) +} + func getDate(t time.Time) int { return (t.Year() * 10000) + (int(t.Month()) * 100) + t.Day() } diff --git a/internal/backend/darkstorm_test.go b/internal/backend/darkstorm_test.go index c33a15a..476153f 100644 --- a/internal/backend/darkstorm_test.go +++ b/internal/backend/darkstorm_test.go @@ -1,23 +1,8 @@ -package backend +package backend_test import ( - "fmt" "testing" - "time" - - "github.com/google/uuid" ) func TestStuff(t *testing.T) { - for i := 0; i < 50; i++ { - go func() { - id, err := uuid.NewV7() - if err != nil { - fmt.Println(err) - } - fmt.Println(id.String()) - }() - } - time.Sleep(3 * time.Second) - t.Fatal("end") } diff --git a/internal/backend/db/sql.go b/internal/backend/db/sql.go new file mode 100644 index 0000000..ff2015b --- /dev/null +++ b/internal/backend/db/sql.go @@ -0,0 +1,6 @@ +package db + +/* +TODO +I don't like SQL, lol. +*/ \ No newline at end of file diff --git a/internal/backend/db/valkey.go b/internal/backend/db/valkey.go index 10060e9..9666e43 100644 --- a/internal/backend/db/valkey.go +++ b/internal/backend/db/valkey.go @@ -1 +1,11 @@ -package db \ No newline at end of file +package db + +/* +TODO +Currently there isn't a clean way to implement this (as far as I can tell). +valkey-go relies on an internal library for it's command builder, which makes it impossible to +use properly for generics without manually writing out the Index command. I could probably do this, but +it's a pain. +valkey-go does have a Generic Object Mapping library (valkey-go/om), but it requires a Version field +on every struct which would be confusing if I did add it to all my structs and Go doesn't allow anonymous generics inside structs +*/ \ No newline at end of file diff --git a/main.go b/main.go index da29a2c..b476897 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,22 @@ package main +import ( + "crypto/tls" + "flag" + "log" +) + func main() { + mongoURL := flag.String("mongo", "", "Enables MongoDB usage for darkstorm-backend.") + webRoot := flag.String("web-root", "", "Sets root directory of web server.") + flag.Parse() + if flag.NArg() != 1 { + log.Fatal("You must specify key directory. ex: darkstorm-server /etc/web-keys") + } + if *mongoURL != "" { + } + mongoCert, err := tls.LoadX509KeyPair(flag.Arg(0)+"mongo.pem", flag.Arg(0)+"key.pem") + if err != nil { + + } } From fdd8d4905510cbb8c7c264d49404d3b02ee5f73b Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Mon, 17 Jun 2024 07:28:33 -0500 Subject: [PATCH 16/17] Working on blog stuff --- LICENSE | 2 +- internal/backend/header.go | 2 + internal/blog/README.md | 83 ++++++++++++++++++ internal/blog/author.go | 54 ++++++++++++ internal/blog/blog.go | 173 +++++++++++++++++++++++++++++++++++++ internal/blog/main.go | 27 ++++++ main.go | 45 ++++++++-- portfolio.go | 9 ++ 8 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 internal/blog/README.md create mode 100644 internal/blog/author.go create mode 100644 internal/blog/blog.go create mode 100644 internal/blog/main.go create mode 100644 portfolio.go diff --git a/LICENSE b/LICENSE index 02776f9..6b6c897 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Caleb Gardner +Copyright (c) 2024 Caleb Gardner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/internal/backend/header.go b/internal/backend/header.go index 09ea247..6c47ab5 100644 --- a/internal/backend/header.go +++ b/internal/backend/header.go @@ -80,6 +80,8 @@ func (b *Backend) ParseHeader(r *http.Request) (*ParsedHeader, error) { // If the check if failed, ReturnError will be called and the returned *ParsedHeader will be nil. // If token is present but invalid, no error will be returned just ParsedHeader.User will be nil. // The error return will only be populated on "internal" errors and should *probably* be logged. +// +// This function does not check the Key's appID so after calling VerifyHeader it's recommended to check the Key's appID. func (b *Backend) VerifyHeader(w http.ResponseWriter, r *http.Request, keyPerm string, allowManagementKey bool) (*ParsedHeader, error) { hdr, err := b.ParseHeader(r) if hdr == nil || hdr.Key == nil { diff --git a/internal/blog/README.md b/internal/blog/README.md new file mode 100644 index 0000000..9343b9e --- /dev/null +++ b/internal/blog/README.md @@ -0,0 +1,83 @@ +# Blog module + +A simple blog module for darkstorm-backend. + +## Requests + +### Author info + +> GET /author/{authorID} + +```json +``` + +### Blog + +#### Specific blog + +> GET /blog/{blogID} + +Return: + +```json +{ + id: "blogID", + createTime: 0, // creation time in Unix format + updateTime: 0, // last update time in Unix format + author: "authorID", + favicon: "favicon url", + title: "blog title", + blog: "blog", // blog will have been converted to HTML +} +``` + +#### Latest blogs + +> GET /blog?page=0 + +Will return up to 5 blogs. `page` query is optional (implies 0 if not set). + +Return: + +```json +{ + num: 1, // Number of returned results, returns up to 5 results + blogs: [ + { + id: "blogID", + createTime: 0, // creation time in Unix format + updateTime: 0, // last update time in Unix format + author: "authorID", + favicon: "favicon url", + title: "blog title", + blog: "blog", // blog will have been converted to HTML + } + ... + ] +} +``` + +#### Blog List + +> GET /blog/list?page=0 + +Will return up to 50 IDs. `page` query is optional (implies 0 if not set). + +Return: + +```json +{ + num: 1, // Number of returned results, returns up to 50 results + blogList: [ + { + id: "blogID", + createTime: 0, // Unix format + }, + { + id: "blogID", + createTime: 0, // Unix format + }, + ... + ] +} +``` diff --git a/internal/blog/author.go b/internal/blog/author.go new file mode 100644 index 0000000..a6f87ba --- /dev/null +++ b/internal/blog/author.go @@ -0,0 +1,54 @@ +package blog + +import ( + "context" + "log" + "net/http" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type Author struct { + ID string `json:"id" bson:"_id"` + About string `json:"about" bson:"about"` + PicURL string `json:"picurl" bson:"picurl"` +} + +func (b *BlogApp) AboutCaleb() (*Author, error) { + res := b.authCol.FindOne(context.Background(), bson.M{"_id": "caleb_gardner"}) + if res.Err() != nil { + log.Println("error getting about me:", res.Err()) + if res.Err() == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } + return nil, res.Err() + } + var aboutMe Author + err := res.Decode(&aboutMe) + if err != nil { + log.Println("error decoding about me:", res) + return nil, err + } + return &aboutMe, nil +} + +func (b *BlogApp) GetAuthorInfo(w http.ResponseWriter, r *http.Request) { + +} + +func (b *BlogApp) SetAuthorInfo(w http.ResponseWriter, r *http.Request) { + hdr, err := b.back.VerifyHeader(w, r, "managment", true) + if hdr == nil { + if err != nil { + log.Println("error verifying apiKey:", err) + } + return + } + if hdr.Key.AppID != "blog" { + backend.ReturnError(w, http.StatusUnauthorized, "invalidKey", "Application not authorized") + return + } + +} diff --git a/internal/blog/blog.go b/internal/blog/blog.go new file mode 100644 index 0000000..bc25db5 --- /dev/null +++ b/internal/blog/blog.go @@ -0,0 +1,173 @@ +package blog + +import ( + "context" + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Blog struct { + ID string `json:"id" bson:"_id"` + Author string `json:"author" bson:"author"` + Favicon string `json:"favicon" bson:"favicon"` + Title string `json:"title" bson:"title"` + Blog string `json:"blog" bson:"blog"` + CreateTime int `json:"createTime" bson:"createTime"` + UpdateTime int `json:"updateTime" bson:"updateTime"` +} + +func (b *Blog) ConvertBlog() { + //TODO: parse BBCode/Markdown from blog + //b.Blog = bbCodeConvert(b.Blog) +} + +func (b *BlogApp) GetAuthor(blog *Blog) (*Author, error) { + res := b.authCol.FindOne(context.Background(), bson.M{"_id": blog.Author}) + if res.Err() != nil { + if res.Err() == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } + return nil, res.Err() + } + var author Author + err := res.Decode(&author) + return &author, err +} + +func (b *BlogApp) GetBlog(ID string) (*Blog, error) { + res := b.blogCol.FindOne(context.Background(), bson.M{"_id": ID}) + if res.Err() != nil { + if res.Err() == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } + return nil, res.Err() + } + var blog Blog + err := res.Decode(blog) + if err != nil { + return nil, err + } + blog.ConvertBlog() + return &blog, nil +} + +func (b *BlogApp) Blog(w http.ResponseWriter, r *http.Request) { + blogID := r.PathValue("blogID") + if blogID == "" { + backend.ReturnError(w, http.StatusBadRequest, "badRequest", "Must provide a blogID") + return + } + blog, err := b.GetBlog(blogID) + if err != nil { + if err == backend.ErrNotFound { + backend.ReturnError(w, http.StatusNotFound, "notFound", "Not blog found with the given ID") + return + } + log.Println("error getting blog:", err) + backend.ReturnError(w, http.StatusInternalServerError, "internal", "Server error") + return + } + json.NewEncoder(w).Encode(blog) +} + +func (b *BlogApp) GetLatestBlogs(page int64) ([]Blog, error) { + res, err := b.blogCol.Find(context.Background(), bson.M{}, options.Find(). + SetSort(bson.M{"createTime": 1}). + SetLimit(5). + SetSkip(page*5)) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } + return nil, err + } + var out []Blog + err = res.All(context.Background(), &out) + if err != nil { + return nil, err + } + for i := range out { + out[i].ConvertBlog() + } + return out, nil +} + +func (b *BlogApp) LatestBlogs(w http.ResponseWriter, r *http.Request) { + var page int + var err error + pagQuery := r.URL.Query().Get("page") + if pagQuery != "" { + page, err = strconv.Atoi(pagQuery) + if err != nil { + page = 0 + } + } + blogs, err := b.GetLatestBlogs(int64(page)) + if err != nil && err != backend.ErrNotFound { + backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error") + return + } + var ret struct { + Blogs []Blog `json:"blogs"` + Num int `json:"num"` + } + ret.Num = len(blogs) + ret.Blogs = blogs + json.NewEncoder(w).Encode(ret) +} + +type BlogListResult struct { + ID string `json:"id" bson:"_id"` + CreateTime int `json:"createTime" bson:"createTime"` +} + +func (b *BlogApp) GetBlogList(page int64) ([]BlogListResult, error) { + res, err := b.blogCol.Find(context.Background(), bson.M{}, options.Find(). + SetProjection(bson.M{"_id": 1, "createTime": 1}). + SetSort(bson.M{"createTime": 1}). + SetLimit(50). + SetSkip(page*50)) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, backend.ErrNotFound + } + return nil, err + } + var out []BlogListResult + err = res.All(context.Background(), &out) + if err != nil { + return nil, err + } + return out, nil +} + +func (b *BlogApp) BlogList(w http.ResponseWriter, r *http.Request) { + var page int + var err error + pagQuery := r.URL.Query().Get("page") + if pagQuery != "" { + page, err = strconv.Atoi(pagQuery) + if err != nil { + page = 0 + } + } + blogList, err := b.GetBlogList(int64(page)) + if err != nil && err != backend.ErrNotFound { + backend.ReturnError(w, http.StatusInternalServerError, "internal", "internal error") + return + } + var ret struct { + BlogList []BlogListResult `json:"blogList"` + Num int `json:"num"` + } + ret.Num = len(blogList) + ret.BlogList = blogList + json.NewEncoder(w).Encode(ret) +} diff --git a/internal/blog/main.go b/internal/blog/main.go new file mode 100644 index 0000000..1a02a27 --- /dev/null +++ b/internal/blog/main.go @@ -0,0 +1,27 @@ +package blog + +import ( + "net/http" + + "github.com/CalebQ42/darkstorm-server/internal/backend" + "go.mongodb.org/mongo-driver/mongo" +) + +type BlogApp struct { + back *backend.Backend + blogCol *mongo.Collection + authCol *mongo.Collection +} + +func NewBlogApp(b *backend.Backend, db *mongo.Database, mux *http.ServeMux) *BlogApp { + out := &BlogApp{ + back: b, + blogCol: db.Collection("blog"), + authCol: db.Collection("author"), + } + // setup mux + mux.HandleFunc("GET /blog/", out.LatestBlogs) + mux.HandleFunc("GET /blog/{blogID}", out.Blog) + //TODO + return out +} diff --git a/main.go b/main.go index b476897..660fd99 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,55 @@ package main import ( + "context" "crypto/tls" "flag" "log" + "net/http" + "path/filepath" + + "github.com/CalebQ42/darkstorm-server/internal/blog" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + mongoClient *mongo.Client + blogApp *blog.BlogApp ) func main() { - mongoURL := flag.String("mongo", "", "Enables MongoDB usage for darkstorm-backend.") + mongoURL := flag.String("mongo", "", "Enables MongoDB usage for Darkstorm backend.") webRoot := flag.String("web-root", "", "Sets root directory of web server.") flag.Parse() if flag.NArg() != 1 { log.Fatal("You must specify key directory. ex: darkstorm-server /etc/web-keys") } - if *mongoURL != "" { - } - mongoCert, err := tls.LoadX509KeyPair(flag.Arg(0)+"mongo.pem", flag.Arg(0)+"key.pem") - if err != nil { - + if *mongoURL == "" || *webRoot == "" { + log.Fatal("SPECIFY MONGO AND WEB-ROOT OR I WILL DIE (Death noises).") } + mux := http.NewServeMux() + mongoClient = setupMongo(*mongoURL) + setupBackend(mux) + setupWebsite(mux, *webRoot) + http.ListenAndServeTLS(":443", filepath.Join(flag.Arg(0), "cert.pem"), filepath.Join(flag.Arg(0), "key.pem"), mux) +} + +func setupMongo(uri string) *mongo.Client { + mongoCert, err := tls.LoadX509KeyPair(filepath.Join(flag.Arg(0), "mongo.pem"), filepath.Join(flag.Arg(0)+"key.pem")) + if err != nil { + log.Fatal("error loading mongo keys:", err) + } + client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri).SetTLSConfig(&tls.Config{ + Certificates: []tls.Certificate{mongoCert}, + })) + if err != nil { + log.Fatal("error connecting to mongo:", err) + } + return client +} + +func setupWebsite(mux *http.ServeMux, root string) {} + +func setupBackend(mux *http.ServeMux) { } diff --git a/portfolio.go b/portfolio.go new file mode 100644 index 0000000..b12e6cf --- /dev/null +++ b/portfolio.go @@ -0,0 +1,9 @@ +package main + +import ( + "go.mongodb.org/mongo-driver/mongo" +) + +func portfolio(client *mongo.Client) { + //TODO +} From fa9330a95919963c12b782ae6b2129a86c590ac2 Mon Sep 17 00:00:00 2001 From: Caleb Gardner Date: Mon, 17 Jun 2024 18:01:37 -0500 Subject: [PATCH 17/17] More Stuff --- internal/blog/README.md | 75 +++++++++++++++++++++++++++++++++++++++++ internal/blog/author.go | 4 +-- internal/blog/blog.go | 8 +++++ internal/blog/main.go | 5 ++- 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/internal/blog/README.md b/internal/blog/README.md index 9343b9e..28e1825 100644 --- a/internal/blog/README.md +++ b/internal/blog/README.md @@ -6,9 +6,43 @@ A simple blog module for darkstorm-backend. ### Author info +#### Get author info + > GET /author/{authorID} ```json +{ + id: "authorID", + about: "about", + picurl: "picture URL" +} +``` + +#### Update author info + +> POST /author/{authorID} + +Must have a auth token for a user with the `"blog": "admin"` permission. + +```json +{ + about: "about", + picurl: "picture url" +} +``` + +#### Add Author info + +> POST /author + +Must have a auth token for a user with the `"blog": "admin"` permission. + +```json +{ + id: "authorID", + about: "about", + picurl: "picture URL" +} ``` ### Blog @@ -31,6 +65,47 @@ Return: } ``` +#### Create blog + +Request: + +> POST /blog + +Must have a auth token for a user with the `"blog": "admin"` permission. + +```json +{ + author: "authorID", + favicon: "favicon url", + title: "blog title", + blog: "blog", // blog will have been converted to HTML +} +``` + +Return: + +```json +{ + id: "blogID" +} +``` + +#### Update blog + +Request: + +> POST /blog/{blogID} + +Must have a auth token for a user with the `"blog": "admin"` permission. + +```json +{ + favicon: "new icon", + title: "new title", + blog: "new blog content" +} +``` + #### Latest blogs > GET /blog?page=0 diff --git a/internal/blog/author.go b/internal/blog/author.go index a6f87ba..9aae509 100644 --- a/internal/blog/author.go +++ b/internal/blog/author.go @@ -16,7 +16,7 @@ type Author struct { PicURL string `json:"picurl" bson:"picurl"` } -func (b *BlogApp) AboutCaleb() (*Author, error) { +func (b *BlogApp) AboutMe() (*Author, error) { res := b.authCol.FindOne(context.Background(), bson.M{"_id": "caleb_gardner"}) if res.Err() != nil { log.Println("error getting about me:", res.Err()) @@ -35,7 +35,7 @@ func (b *BlogApp) AboutCaleb() (*Author, error) { } func (b *BlogApp) GetAuthorInfo(w http.ResponseWriter, r *http.Request) { - + //TODO } func (b *BlogApp) SetAuthorInfo(w http.ResponseWriter, r *http.Request) { diff --git a/internal/blog/blog.go b/internal/blog/blog.go index bc25db5..e4369cd 100644 --- a/internal/blog/blog.go +++ b/internal/blog/blog.go @@ -77,6 +77,14 @@ func (b *BlogApp) Blog(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(blog) } +func (b *BlogApp) CreateBlog(w http.ResponseWriter, r *http.Request) { + //TODO +} + +func (b *BlogApp) UpdateBlog(w http.ResponseWriter, r *http.Request) { + //TODO +} + func (b *BlogApp) GetLatestBlogs(page int64) ([]Blog, error) { res, err := b.blogCol.Find(context.Background(), bson.M{}, options.Find(). SetSort(bson.M{"createTime": 1}). diff --git a/internal/blog/main.go b/internal/blog/main.go index 1a02a27..68b5494 100644 --- a/internal/blog/main.go +++ b/internal/blog/main.go @@ -20,8 +20,11 @@ func NewBlogApp(b *backend.Backend, db *mongo.Database, mux *http.ServeMux) *Blo authCol: db.Collection("author"), } // setup mux - mux.HandleFunc("GET /blog/", out.LatestBlogs) + mux.HandleFunc("GET /blog", out.LatestBlogs) + mux.HandleFunc("GET /blog/list", out.BlogList) mux.HandleFunc("GET /blog/{blogID}", out.Blog) + + mux.HandleFunc("POST /blog", out.CreateBlog) //TODO return out }