From 1711530ddad3a442b4a5028dd89e12b6654dcea4 Mon Sep 17 00:00:00 2001 From: Georgios Komninos Date: Sun, 8 Aug 2021 20:55:59 +0300 Subject: [PATCH] Fixes security issue & Adds support to sent configuration via email (#83) --- README.md | 12 ++++ custom/js/helper.js | 3 + docker-compose.yaml | 8 ++- emailer/interface.go | 10 +++ emailer/sendgrid.go | 54 ++++++++++++++++ go.mod | 2 + go.sum | 4 ++ handler/routes.go | 76 +++++++++++++--------- handler/session.go | 28 +++++--- main.go | 55 ++++++++++------ router/router.go | 6 +- templates/clients.html | 143 +++++++++++++++++++++++++++++++++++++---- util/config.go | 10 ++- 13 files changed, 335 insertions(+), 76 deletions(-) create mode 100644 emailer/interface.go create mode 100644 emailer/sendgrid.go diff --git a/README.md b/README.md index 94791b7..2d4713b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,18 @@ You can take a look at this example of [docker-compose.yml](https://github.com/n ``` docker-compose up ``` +### Environment Variables + + +Set the `SESSION_SECRET` environment variable to a random value. + +In order to sent the wireguard configuration to clients via email (using sendgrid api) set the following environment variables + +``` +SENDGRID_API_KEY: Your sendgrid api key +EMAIL_FROM: the email address you registered on sendgrid +EMAIL_FROM_NAME: the sender's email address +``` ### Using binary file diff --git a/custom/js/helper.js b/custom/js/helper.js index a624591..feab9ac 100644 --- a/custom/js/helper.js +++ b/custom/js/helper.js @@ -29,6 +29,9 @@ function renderClientList(data) {
+ diff --git a/docker-compose.yaml b/docker-compose.yaml index 72da096..1494a72 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,8 +2,14 @@ version: '3' services: wg: - image: ngoduykhanh/wireguard-ui:latest + build: . + #image: ngoduykhanh/wireguard-ui:latest container_name: wgui + environment: + - SENDGRID_API_KEY + - EMAIL_FROM + - EMAIL_FROM_NAME + - SESSION_SECRET ports: - 5000:5000 logging: diff --git a/emailer/interface.go b/emailer/interface.go new file mode 100644 index 0000000..5a486fc --- /dev/null +++ b/emailer/interface.go @@ -0,0 +1,10 @@ +package emailer + +type Attachment struct { + Name string + Data []byte +} + +type Emailer interface { + Send(toName string, to string, subject string, content string, attachments []Attachment) error +} diff --git a/emailer/sendgrid.go b/emailer/sendgrid.go new file mode 100644 index 0000000..864c953 --- /dev/null +++ b/emailer/sendgrid.go @@ -0,0 +1,54 @@ +package emailer + +import ( + "encoding/base64" + + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" +) + +type SendgridApiMail struct { + apiKey string + fromName string + from string +} + +func NewSendgridApiMail(apiKey, fromName, from string) *SendgridApiMail { + ans := SendgridApiMail{apiKey: apiKey, fromName: fromName, from: from} + return &ans +} + +func (o *SendgridApiMail) Send(toName string, to string, subject string, content string, attachments []Attachment) error { + m := mail.NewV3Mail() + + mailFrom := mail.NewEmail(o.fromName, o.from) + mailContent := mail.NewContent("text/html", content) + mailTo := mail.NewEmail(toName, to) + + m.SetFrom(mailFrom) + m.AddContent(mailContent) + + personalization := mail.NewPersonalization() + personalization.AddTos(mailTo) + personalization.Subject = subject + + m.AddPersonalizations(personalization) + + toAdd := make([]*mail.Attachment, 0, len(attachments)) + for i := range attachments { + var att mail.Attachment + encoded := base64.StdEncoding.EncodeToString(attachments[i].Data) + att.SetContent(encoded) + att.SetType("text/plain") + att.SetFilename(attachments[i].Name) + att.SetDisposition("attachment") + toAdd = append(toAdd, &att) + } + + m.AddAttachment(toAdd...) + request := sendgrid.GetRequest(o.apiKey, "/v3/mail/send", "https://api.sendgrid.com") + request.Method = "POST" + request.Body = mail.GetRequestBody(m) + _, err := sendgrid.API(request) + return err +} diff --git a/go.mod b/go.mod index 1678c52..282cb02 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( github.com/leodido/go-urn v1.2.0 // indirect github.com/rs/xid v1.2.1 github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba + github.com/sendgrid/rest v2.6.4+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.10.0+incompatible github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf gopkg.in/go-playground/assert.v1 v1.2.1 // indirect diff --git a/go.sum b/go.sum index 9a0e435..224f330 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,10 @@ github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba h1:8QAc9wFAf2b/9cAXskm0wBylObZ0bTpRcaP7ThjLPVQ= github.com/sdomino/scribble v0.0.0-20191024200645-4116320640ba/go.mod h1:W6zxGUBCXRR5QugSd/nFcFVmwoGnvpjiNY/JwT03Wew= +github.com/sendgrid/rest v2.6.4+incompatible h1:lq6gAQxLwVBf3mVyCCSHI6mgF+NfaJFJHjT0kl6SSo8= +github.com/sendgrid/rest v2.6.4+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.10.0+incompatible h1:aSYyurHxEZSDy7kxhvZ4fH0inNkEEmRssZNbAmETR2c= +github.com/sendgrid/sendgrid-go v3.10.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= diff --git a/handler/routes.go b/handler/routes.go index d3029e1..22df3d7 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -1,6 +1,7 @@ package handler import ( + "encoding/base64" "encoding/json" "fmt" "net/http" @@ -13,6 +14,7 @@ import ( "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" + "github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/util" "github.com/rs/xid" @@ -77,8 +79,6 @@ func Logout() echo.HandlerFunc { // WireGuardClients handler func WireGuardClients() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) clientDataList, err := util.GetClients(true) if err != nil { @@ -97,8 +97,6 @@ func WireGuardClients() echo.HandlerFunc { // GetClients handler return a list of Wireguard client data func GetClients() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) clientDataList, err := util.GetClients(true) if err != nil { @@ -114,8 +112,6 @@ func GetClients() echo.HandlerFunc { // GetClient handler return a of Wireguard client data func GetClient() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) clientID := c.Param("id") clientData, err := util.GetClientByID(clientID, true) @@ -130,8 +126,6 @@ func GetClient() echo.HandlerFunc { // NewClient handler func NewClient() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) client := new(model.Client) c.Bind(client) @@ -194,11 +188,53 @@ func NewClient() echo.HandlerFunc { } } +// EmailClient handler to sent the configuration via email +func EmailClient(mailer emailer.Emailer, emailSubject, emailContent string) echo.HandlerFunc { + type clientIdEmailPayload struct { + ID string `json:"id"` + Email string `json:"email"` + } + + return func(c echo.Context) error { + var payload clientIdEmailPayload + c.Bind(&payload) + // TODO validate email + + clientData, err := util.GetClientByID(payload.ID, true) + if err != nil { + log.Errorf("Cannot generate client id %s config file for downloading: %v", payload.ID, err) + return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) + } + + // build config + server, _ := util.GetServer() + globalSettings, _ := util.GetGlobalSettings() + config := util.BuildClientConfig(*clientData.Client, server, globalSettings) + + cfg_att := emailer.Attachment{"wg0.conf", []byte(config)} + qrdata, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(clientData.QRCode, "data:image/png;base64,")) + if err != nil { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "decoding: " + err.Error()}) + } + qr_att := emailer.Attachment{"wg.png", qrdata} + err = mailer.Send( + clientData.Client.Name, + payload.Email, + emailSubject, + emailContent, + []emailer.Attachment{cfg_att, qr_att}, + ) + if err != nil { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) + } + + return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Email sent successfully"}) + } +} + // UpdateClient handler to update client information func UpdateClient() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) _client := new(model.Client) c.Bind(_client) @@ -257,8 +293,6 @@ func UpdateClient() echo.HandlerFunc { // SetClientStatus handler to enable / disable a client func SetClientStatus() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) data := make(map[string]interface{}) err := json.NewDecoder(c.Request().Body).Decode(&data) @@ -320,8 +354,6 @@ func DownloadClient() echo.HandlerFunc { // RemoveClient handler func RemoveClient() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) client := new(model.Client) c.Bind(client) @@ -346,8 +378,6 @@ func RemoveClient() echo.HandlerFunc { // WireGuardServer handler func WireGuardServer() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) server, err := util.GetServer() if err != nil { @@ -365,8 +395,6 @@ func WireGuardServer() echo.HandlerFunc { // WireGuardServerInterfaces handler func WireGuardServerInterfaces() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) serverInterface := new(model.ServerInterface) c.Bind(serverInterface) @@ -396,8 +424,6 @@ func WireGuardServerInterfaces() echo.HandlerFunc { // WireGuardServerKeyPair handler to generate private and public keys func WireGuardServerKeyPair() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) // gen Wireguard key pair key, err := wgtypes.GeneratePrivateKey() @@ -428,8 +454,6 @@ func WireGuardServerKeyPair() echo.HandlerFunc { // GlobalSettings handler func GlobalSettings() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) globalSettings, err := util.GetGlobalSettings() if err != nil { @@ -446,8 +470,6 @@ func GlobalSettings() echo.HandlerFunc { // GlobalSettingSubmit handler to update the global settings func GlobalSettingSubmit() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) globalSettings := new(model.GlobalSetting) c.Bind(globalSettings) @@ -477,8 +499,6 @@ func GlobalSettingSubmit() echo.HandlerFunc { // MachineIPAddresses handler to get local interface ip addresses func MachineIPAddresses() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) // get private ip addresses interfaceList, err := util.GetInterfaceIPs() @@ -503,8 +523,6 @@ func MachineIPAddresses() echo.HandlerFunc { // SuggestIPAllocation handler to get the list of ip address for client func SuggestIPAllocation() echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) server, err := util.GetServer() if err != nil { @@ -541,8 +559,6 @@ func SuggestIPAllocation() echo.HandlerFunc { // ApplyServerConfig handler to write config file and restart Wireguard server func ApplyServerConfig(tmplBox *rice.Box) echo.HandlerFunc { return func(c echo.Context) error { - // access validation - validSession(c) server, err := util.GetServer() if err != nil { diff --git a/handler/session.go b/handler/session.go index 6985327..10042ac 100644 --- a/handler/session.go +++ b/handler/session.go @@ -9,22 +9,32 @@ import ( "github.com/ngoduykhanh/wireguard-ui/util" ) -// validSession to redirect user to the login page if they are not authenticated or session expired. -func validSession(c echo.Context) { - if !util.DisableLogin { - sess, _ := session.Get("session", c) - cookie, err := c.Cookie("session_token") - if err != nil || sess.Values["session_token"] != cookie.Value { +func ValidSession(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if !isValidSession(c) { nextURL := c.Request().URL - if nextURL != nil { - c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/login?next=%s", c.Request().URL)) + if nextURL != nil && c.Request().Method == http.MethodGet { + return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/login?next=%s", c.Request().URL)) } else { - c.Redirect(http.StatusTemporaryRedirect, "/login") + return c.Redirect(http.StatusTemporaryRedirect, "/login") } } + return next(c) } } +func isValidSession(c echo.Context) bool { + if util.DisableLogin { + return true + } + sess, _ := session.Get("session", c) + cookie, err := c.Cookie("session_token") + if err != nil || sess.Values["session_token"] != cookie.Value { + return false + } + return true +} + // currentUser to get username of logged in user func currentUser(c echo.Context) string { if util.DisableLogin { diff --git a/main.go b/main.go index 0553272..2e9e5cd 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,13 @@ import ( "flag" "fmt" "net/http" + "os" "time" rice "github.com/GeertJohan/go.rice" "github.com/labstack/echo/v4" + + "github.com/ngoduykhanh/wireguard-ui/emailer" "github.com/ngoduykhanh/wireguard-ui/handler" "github.com/ngoduykhanh/wireguard-ui/router" "github.com/ngoduykhanh/wireguard-ui/util" @@ -21,6 +24,15 @@ var ( buildTime = fmt.Sprintf(time.Now().UTC().Format("01-02-2006 15:04:05")) ) +const ( + defaultEmailSubject = "Your wireguard configuration" + defaultEmailContent = `Hi,
+

in this email you can file your personal configuration for our wireguard server.

+ +

Best

+` +) + func init() { // command-line flags flagDisableLogin := flag.Bool("disable-login", false, "Disable login page. Turn off authentication.") @@ -30,6 +42,10 @@ func init() { // update runtime config util.DisableLogin = *flagDisableLogin util.BindAddress = *flagBindAddress + util.SendgridApiKey = os.Getenv("SENDGRID_API_KEY") + util.EmailFrom = os.Getenv("EMAIL_FROM") + util.EmailFromName = os.Getenv("EMAIL_FROM_NAME") + util.SessionSecret = []byte(os.Getenv("SESSION_SECRET")) // print app information fmt.Println("Wireguard UI") @@ -60,31 +76,34 @@ func main() { assetHandler := http.FileServer(rice.MustFindBox("assets").HTTPBox()) // register routes - app := router.New(tmplBox, extraData) + app := router.New(tmplBox, extraData, util.SessionSecret) - app.GET("/", handler.WireGuardClients()) + app.GET("/", handler.WireGuardClients(), handler.ValidSession) if !util.DisableLogin { app.GET("/login", handler.LoginPage()) app.POST("/login", handler.Login()) } - app.GET("/logout", handler.Logout()) - app.POST("/new-client", handler.NewClient()) - app.POST("/update-client", handler.UpdateClient()) - app.POST("/client/set-status", handler.SetClientStatus()) - app.POST("/remove-client", handler.RemoveClient()) - app.GET("/download", handler.DownloadClient()) - app.GET("/wg-server", handler.WireGuardServer()) - app.POST("wg-server/interfaces", handler.WireGuardServerInterfaces()) - app.POST("wg-server/keypair", handler.WireGuardServerKeyPair()) - app.GET("/global-settings", handler.GlobalSettings()) - app.POST("/global-settings", handler.GlobalSettingSubmit()) - app.GET("/api/clients", handler.GetClients()) - app.GET("/api/client/:id", handler.GetClient()) - app.GET("/api/machine-ips", handler.MachineIPAddresses()) - app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation()) - app.GET("/api/apply-wg-config", handler.ApplyServerConfig(tmplBox)) + sendmail := emailer.NewSendgridApiMail(util.SendgridApiKey, util.EmailFromName, util.EmailFrom) + + app.GET("/logout", handler.Logout(), handler.ValidSession) + app.POST("/new-client", handler.NewClient(), handler.ValidSession) + app.POST("/update-client", handler.UpdateClient(), handler.ValidSession) + app.POST("/email-client", handler.EmailClient(sendmail, defaultEmailSubject, defaultEmailContent), handler.ValidSession) + app.POST("/client/set-status", handler.SetClientStatus(), handler.ValidSession) + app.POST("/remove-client", handler.RemoveClient(), handler.ValidSession) + app.GET("/download", handler.DownloadClient(), handler.ValidSession) + app.GET("/wg-server", handler.WireGuardServer(), handler.ValidSession) + app.POST("wg-server/interfaces", handler.WireGuardServerInterfaces(), handler.ValidSession) + app.POST("wg-server/keypair", handler.WireGuardServerKeyPair(), handler.ValidSession) + app.GET("/global-settings", handler.GlobalSettings(), handler.ValidSession) + app.POST("/global-settings", handler.GlobalSettingSubmit(), handler.ValidSession) + app.GET("/api/clients", handler.GetClients(), handler.ValidSession) + app.GET("/api/client/:id", handler.GetClient(), handler.ValidSession) + app.GET("/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) + app.GET("/api/suggest-client-ips", handler.SuggestIPAllocation(), handler.ValidSession) + app.GET("/api/apply-wg-config", handler.ApplyServerConfig(tmplBox), handler.ValidSession) // servers other static files app.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) diff --git a/router/router.go b/router/router.go index 2bd634e..bb14431 100644 --- a/router/router.go +++ b/router/router.go @@ -6,7 +6,7 @@ import ( "reflect" "text/template" - "github.com/GeertJohan/go.rice" + rice "github.com/GeertJohan/go.rice" "github.com/gorilla/sessions" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -44,9 +44,9 @@ func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c } // New function -func New(tmplBox *rice.Box, extraData map[string]string) *echo.Echo { +func New(tmplBox *rice.Box, extraData map[string]string, secret []byte) *echo.Echo { e := echo.New() - e.Use(session.Middleware(sessions.NewCookieStore([]byte("secret")))) + e.Use(session.Middleware(sessions.NewCookieStore(secret))) // read html template file to string tmplBaseString, err := tmplBox.String("base.html") diff --git a/templates/clients.html b/templates/clients.html index b4e26bd..cc6b287 100644 --- a/templates/clients.html +++ b/templates/clients.html @@ -30,6 +30,34 @@ Wireguard Clients
+ +