Support new mail:inbox perm

This commit is contained in:
Melon 2023-11-19 23:36:45 +00:00
parent 8b1e000ca3
commit 0fdb91d224
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
14 changed files with 489 additions and 80 deletions

View File

@ -10,7 +10,11 @@ import (
"time"
)
var upgrader = websocket.Upgrader{}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func SetupApiServer(listen string, auth *AuthChecker, send Smtp, recv Imap) *http.Server {
r := httprouter.New()
@ -75,10 +79,17 @@ func SetupApiServer(listen string, auth *AuthChecker, send Smtp, recv Imap) *htt
return
}
mailInboxes := authUser.Claims.Perms.Search("mail:inbox=*")
if len(mailInboxes) != 1 {
_ = c.WriteJSON(map[string]string{"error": "Authentication should only contain one owned inbox"})
return
}
// open imap client
client, err := recv.MakeClient(authUser.Subject)
client, err := recv.MakeClient(mailInboxes[0][len("mail:inbox="):])
if err != nil {
_ = c.WriteJSON(map[string]string{"error": "Making client failed"})
log.Println("Making a client failed:", err)
_ = c.WriteJSON(map[string]string{"error": "Making a client failed"})
return
}

View File

@ -3,9 +3,9 @@ package api
import (
"crypto/subtle"
"errors"
"github.com/1f349/mjwt"
"github.com/1f349/mjwt/auth"
"github.com/1f349/violet/utils"
"github.com/MrMelon54/mjwt"
"github.com/MrMelon54/mjwt/auth"
"github.com/julienschmidt/httprouter"
"net/http"
)

View File

@ -8,8 +8,8 @@ import (
"fmt"
postfixLookup "github.com/1f349/lotus/postfix-lookup"
"github.com/1f349/lotus/sendmail"
"github.com/MrMelon54/mjwt/auth"
"github.com/MrMelon54/mjwt/claims"
"github.com/1f349/mjwt/auth"
"github.com/1f349/mjwt/claims"
"github.com/emersion/go-message/mail"
"github.com/golang-jwt/jwt/v4"
"github.com/julienschmidt/httprouter"

View File

@ -3,9 +3,9 @@ package main
import (
"flag"
"github.com/1f349/lotus/api"
"github.com/1f349/mjwt"
"github.com/1f349/violet/utils"
exitReload "github.com/MrMelon54/exit-reload"
"github.com/MrMelon54/mjwt"
"gopkg.in/yaml.v3"
"log"
"os"

19
go.mod
View File

@ -3,26 +3,29 @@ module github.com/1f349/lotus
go 1.21.0
require (
github.com/1f349/violet v0.0.7
github.com/1f349/mjwt v0.2.0
github.com/1f349/violet v0.0.12
github.com/MrMelon54/exit-reload v0.0.1
github.com/MrMelon54/mjwt v0.1.1
github.com/emersion/go-imap v1.2.1
github.com/emersion/go-message v0.16.0
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.17.0
github.com/emersion/go-message v0.17.0
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
github.com/emersion/go-smtp v0.19.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/gorilla/websocket v1.5.0
github.com/google/uuid v1.4.0
github.com/gorilla/websocket v1.5.1
github.com/hydrogen18/memlistener v1.0.0
github.com/julienschmidt/httprouter v1.3.0
github.com/rs/cors v1.10.1
github.com/stretchr/testify v1.8.4
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/becheran/wildmatch-go v1.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

74
go.sum
View File

@ -1,49 +1,87 @@
github.com/1f349/violet v0.0.7 h1:FxCAIVjzUzkgGfhGMX7FcvGj+kaJky45PnLfqKNgA8M=
github.com/1f349/violet v0.0.7/go.mod h1:YfKZX9p55Uot8iSDnbqQbAgU717H0rFNo8ieu2wbxI4=
github.com/1f349/mjwt v0.2.0 h1:1c3+J05RRBsClGxA91SzT3I2DkwasGA4OgLcIeXWmq4=
github.com/1f349/mjwt v0.2.0/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
github.com/1f349/violet v0.0.12 h1:VIiVYfKptCYJvwaJHFgtOyTUOURRMIltGp5Blw9+isY=
github.com/1f349/violet v0.0.12/go.mod h1:8xyh96shYiSBkwumvG/KkiY78tAhxiOomDlT7phZAbA=
github.com/MrMelon54/exit-reload v0.0.1 h1:sxHa59tNEQMcikwuX2+93lw6Vi1+R7oCRF8a0C3alXc=
github.com/MrMelon54/exit-reload v0.0.1/go.mod h1:PLiSfmUzwdpTTQP3BBfUPhkqPwaIZjx0DuXBnM76Bug=
github.com/MrMelon54/mjwt v0.1.1 h1:m+aTpxbhQCrOPKHN170DQMFR5r938LkviU38unob5Jw=
github.com/MrMelon54/mjwt v0.1.1/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
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-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04=
github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
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.19.0 h1:iVCDtR2/JY3RpKoaZ7u6I/sb52S3EzfNHO1fAWVHgng=
github.com/emersion/go-smtp v0.19.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hydrogen18/memlistener v1.0.0 h1:JR7eDj8HD6eXrc5fWLbSUnfcQFL06PYvCc0DKQnWfaU=
github.com/hydrogen18/memlistener v1.0.0/go.mod h1:qEIFzExnS6016fRpRfxrExeVn2gbClQA99gQhnIcdhE=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -2,7 +2,7 @@ package imap
import (
"errors"
"github.com/1f349/lotus/imap/json"
"github.com/1f349/lotus/imap/marshal"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"strconv"
@ -65,7 +65,7 @@ func (c *Client) HandleWS(action string, args []string) (map[string]any, error)
if err != nil {
return nil, err
}
return map[string]any{"type": "fetch", "value": json.ListMessagesJson(fetch)}, nil
return map[string]any{"type": "fetch", "value": marshal.MessageSliceJson(fetch)}, nil
case "move":
// TODO: implementation
case "rename":

View File

@ -1,36 +0,0 @@
package json
import (
"encoding/json"
"github.com/emersion/go-imap"
)
type ListMessagesJson []*imap.Message
func (l ListMessagesJson) MarshalJSON() ([]byte, error) {
a := make([]encodeImapMessage, len(l))
for i := range a {
a[i] = encodeImapMessage(*l[i])
}
return json.Marshal(a)
}
type encodeImapMessage imap.Message
func (e encodeImapMessage) MarshalJSON() ([]byte, error) {
body := make(map[string]imap.Literal, len(e.Body))
for k, v := range e.Body {
body[string(k.FetchItem())] = v
}
return json.Marshal(map[string]any{
"SeqNum": e.SeqNum,
"Items": e.Items,
"Envelope": e.Envelope,
"BodyStructure": e.BodyStructure,
"Flags": e.Flags,
"InternalDate": e.InternalDate,
"Size": e.Size,
"Uid": e.Uid,
"$Body": body,
})
}

View File

@ -2,6 +2,7 @@ package marshal
import (
"encoding/json"
"github.com/1f349/lotus/utils"
"github.com/emersion/go-imap"
)
@ -26,7 +27,7 @@ func (m MessageJson) MarshalJSON() ([]byte, error) {
}
return json.Marshal(map[string]any{
"SeqNum": m.SeqNum,
"Items": m.Items,
"Items": utils.MapKeys(m.Items),
"Envelope": m.Envelope,
"BodyStructure": m.BodyStructure,
"Flags": m.Flags,

View File

@ -1,7 +0,0 @@
package marshal
import imap2 "github.com/emersion/go-imap"
type SeqSet imap2.SeqSet
var json.mar

116
test-server/index.html Normal file
View File

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Client</title>
<script>
let currentLoginPopup = null;
window.addEventListener("message", function (event) {
if (event.origin !== "http:\/\/localhost:9090") return;
if (isObject(event.data)) {
document.getElementById("someTextArea").textContent = JSON.stringify(event.data, null, 2);
let perms = document.getElementById("somePerms");
while (perms.childNodes.length > 0) {
perms.childNodes.item(0).remove();
}
window.currentTokens = event.data.tokens;
let jwt = parseJwt(event.data.tokens.access);
if (jwt.per != null) {
jwt.per.forEach(function (x) {
let a = document.createElement("li");
a.textContent = x;
perms.appendChild(a);
});
}
if (currentLoginPopup) currentLoginPopup.close();
return;
}
alert("Failed to log user in: the login data was probably corrupted");
});
function parseJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
function isObject(obj) {
return obj != null && obj.constructor.name === "Object"
}
function popupCenterScreen(url, title, w, h, focus) {
const top = (screen.availHeight - h) / 4, left = (screen.availWidth - w) / 2;
const popup = openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
if (focus === true && window.focus) popup.focus();
return popup;
}
function openWindow(url, winnm, options) {
var wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
var wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
var top = 0, left = 0;
var result;
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
if (options) {
options = options.replace("top=" + top, "top=" + (parseInt(top) + wTop));
options = options.replace("left=" + left, "left=" + (parseInt(left) + wLeft));
w = window.open(url, winnm, options);
} else w = window.open(url, winnm);
return w;
}
function firstAvailableValue(arr) {
for (var i = 0; i < arr.length; i++)
if (typeof arr[i] != 'undefined')
return arr[i];
}
function doThisThing() {
if (currentLoginPopup) currentLoginPopup.close();
currentLoginPopup = popupCenterScreen('http://localhost:9090/popup?origin=' + encodeURIComponent("http://localhost:2020"), 'Login with Lavender', 500, 500, false);
}
function startWs() {
window.mailWS = new WebSocket('ws://localhost:7070/imap');
window.mailWS.onopen = function() {
window.mailWS.send(JSON.stringify({token: window.currentTokens.access}));
};
}
</script>
<style>
#someTextArea {
width: 400px;
height: 400px;
}
</style>
</head>
<body>
<header>
<h1>Test Client</h1>
</header>
<main>
<div>
<button onclick="doThisThing();">Login</button>
<button onclick="startWs();">Live IMAP</button>
</div>
<div style="display:flex; gap: 2em;">
<div>
<label for="someTextArea"></label><textarea id="someTextArea"></textarea>
</div>
<div>
<p>Permissions:</p>
<ul id="somePerms"></ul>
</div>
<div>
<p>Email output:</p>
<ul id="em-out"></ul>
</div>
</div>
</main>
</body>
</html>

270
test-server/main.go Normal file
View File

@ -0,0 +1,270 @@
package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/1f349/mjwt"
"github.com/1f349/mjwt/auth"
"github.com/1f349/mjwt/claims"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/rs/cors"
)
func main() {
log.Println("Starting test server")
signer, err := mjwt.NewMJwtSignerFromFileOrCreate("Test SSO Service", "private.key.local", rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
go clientServer()
go ssoServer(signer)
go apiServer(signer)
done := make(chan struct{})
<-done
}
func clientServer() {
r := http.NewServeMux()
r.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
http.ServeFile(rw, req, "index.html")
})
log.Println("[Client Server]", http.ListenAndServe(":2020", r))
}
func ssoServer(signer mjwt.Signer) {
r := http.NewServeMux()
r.HandleFunc("/popup", func(w http.ResponseWriter, r *http.Request) {
ps := claims.NewPermStorage()
ps.Set("mail:inbox=admin@localhost")
accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"d0555671-df9d-42d0-a4d6-94b694251f0b"}, 15*time.Minute, auth.AccessTokenClaims{
Perms: ps,
})
if err != nil {
http.Error(w, "Failed to generate access token", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="en">
<head>
<title>Test SSO Service</title>
<script>
let loginData = {
target: "http://localhost:2020",
userinfo: {
"aud": "d0555671-df9d-42d0-a4d6-94b694251f0b",
"email": "admin@localhost",
"email_verified": true,
"name": "Admin",
"preferred_username": "admin",
"sub": "81b99bd7-bf74-4cc2-9133-80ed2393dfe6",
"picture": "http://localhost:5173/1f349.svg",
"updated_at": 0
},
tokens: {
access: "%s",
refresh: "%s",
},
};
window.addEventListener("load", function () {
setTimeout(function() {
window.opener.postMessage(loginData, loginData.target);
},2000);
});
</script>
</head>
<body>
<header>
<h1>Test SSO Service</h1>
</header>
<main id="mainBody">Loading...</main>
</body>
</html>
`, accessToken, "")
})
log.Println("[SSO Server]", http.ListenAndServe(":9090", r))
}
var serveApiCors = cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // allow all origins for api requests
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowedMethods: []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
},
AllowCredentials: true,
})
func apiServer(verify mjwt.Verifier) {
r := http.NewServeMux()
r.Handle("/v1/violet/route", hasPerm(verify, "violet:route", func(rw http.ResponseWriter, req *http.Request) {
m := make([]map[string]any, 0, 40)
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"src": uuid.NewString() + ".example.com",
"dst": "127.0.0.1:8080",
"desc": "This is a test description",
"flags": 181,
"active": true,
})
}
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"src": uuid.NewString() + ".example.org",
"dst": "127.0.0.1:8085",
"desc": "This is a test description",
"flags": 17,
"active": true,
})
}
json.NewEncoder(rw).Encode(m)
}))
r.Handle("/v1/violet/redirect", hasPerm(verify, "violet:redirect", func(rw http.ResponseWriter, req *http.Request) {
m := make([]map[string]any, 0, 40)
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"src": uuid.NewString() + ".example.com",
"dst": "test1.example.com",
"desc": "This is a test description",
"flags": 1,
"code": 308,
"active": true,
})
}
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"src": uuid.NewString() + ".example.org",
"dst": "test2.example.org",
"desc": "This is a test description",
"flags": 3,
"code": 307,
"active": true,
})
}
json.NewEncoder(rw).Encode(m)
}))
r.Handle("/v1/orchid/owned", hasPerm(verify, "orchid:cert", func(rw http.ResponseWriter, req *http.Request) {
m := make(map[int]any, 41)
for i := 0; i < 20; i++ {
u := uuid.NewString()
m[i] = map[string]any{
"id": i + 1,
"auto_renew": true,
"active": true,
"renewing": false,
"renew_failed": false,
"not_after": "2024-02-06T11:52:05Z",
"updated_at": "2023-11-08T07:32:08Z",
"domains": []string{
u + ".example.com",
"*." + u + ".example.com",
},
}
}
for i := 0; i < 20; i++ {
u := uuid.NewString()
m[i+20] = map[string]any{
"id": i + 21,
"auto_renew": false,
"active": false,
"renewing": false,
"renew_failed": false,
"not_after": "2024-02-06T11:52:05Z",
"updated_at": "2023-11-08T07:32:08Z",
"domains": []string{
u + ".example.org",
"*." + u + ".example.org",
},
}
}
u := uuid.NewString()
m[40] = map[string]any{
"id": 41,
"auto_renew": false,
"active": false,
"renewing": false,
"renew_failed": true,
"not_after": "2024-02-06T11:52:05Z",
"updated_at": "2023-11-08T07:32:08Z",
"domains": []string{
u + ".example.org",
"*." + u + ".example.org",
},
}
json.NewEncoder(rw).Encode(m)
}))
r.Handle("/v1/sites", hasPerm(verify, "sites:manage", func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodPost {
defer req.Body.Close()
dec := json.NewDecoder(req.Body)
var m map[string]string
if err := dec.Decode(&m); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
switch m["submit"] {
case "secret":
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "{\"secret\":\"%s\"}\n", uuid.NewString())
return
case "delete-branch":
rw.WriteHeader(http.StatusOK)
}
return
}
m := make([]any, 0, 40)
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"domain": uuid.NewString() + ".example.com",
"branches": []string{"", "beta"},
})
}
for i := 0; i < 20; i++ {
m = append(m, map[string]any{
"domain": uuid.NewString() + ".example.org",
"branches": []string{"", "alpha"},
})
}
json.NewEncoder(rw).Encode(m)
}))
logger := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
log.Println("[API Server]", req.URL.String())
r.ServeHTTP(rw, req)
})
log.Println("[API Server]", http.ListenAndServe(":9095", serveApiCors.Handler(logger)))
}
func hasPerm(verify mjwt.Verifier, perm string, next func(rw http.ResponseWriter, req *http.Request)) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
a := req.Header.Get("Authorization")
if !strings.HasPrefix(a, "Bearer ") {
http.Error(rw, "Missing bearer authorization", http.StatusForbidden)
return
}
_, b, err := mjwt.ExtractClaims[auth.AccessTokenClaims](verify, a[len("Bearer "):])
if err != nil {
http.Error(rw, "Invalid token", http.StatusForbidden)
log.Println("Invalid token:", err)
return
}
if !b.Claims.Perms.Has("violet:route") {
http.Error(rw, "Missing permission", http.StatusForbidden)
return
}
next(rw, req)
})
}

4
test-server/run.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname -- "$(readlink -f -- "$0";)";)"
python3 -m http.server 2020

9
utils/mapkeys.go Normal file
View File

@ -0,0 +1,9 @@
package utils
func MapKeys[M ~map[K]any, K comparable](src M) []K {
dst := make([]K, 0, len(src))
for k := range src {
dst = append(dst, k)
}
return dst
}