mirror of https://github.com/gophish/gophish
parent
699532f256
commit
28cd7a238e
|
@ -13,6 +13,7 @@ node_modules
|
||||||
# Architecture specific extensions/prefixes
|
# Architecture specific extensions/prefixes
|
||||||
*.[568vq]
|
*.[568vq]
|
||||||
[568vq].out
|
[568vq].out
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
*.cgo1.go
|
*.cgo1.go
|
||||||
*.cgo2.c
|
*.cgo2.c
|
||||||
|
@ -27,4 +28,4 @@ gophish_admin.key
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
gophish.db*
|
gophish.db*
|
||||||
gophish
|
gophish
|
||||||
|
|
Binary file not shown.
|
@ -71,6 +71,9 @@ func (as *Server) registerRoutes() {
|
||||||
router.HandleFunc("/import/group", as.ImportGroup)
|
router.HandleFunc("/import/group", as.ImportGroup)
|
||||||
router.HandleFunc("/import/email", as.ImportEmail)
|
router.HandleFunc("/import/email", as.ImportEmail)
|
||||||
router.HandleFunc("/import/site", as.ImportSite)
|
router.HandleFunc("/import/site", as.ImportSite)
|
||||||
|
router.HandleFunc("/webhooks/", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem)))
|
||||||
|
router.HandleFunc("/webhooks/{id:[0-9]+}/validate", mid.Use(as.ValidateWebhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||||
|
router.HandleFunc("/webhooks/{id:[0-9]+}", mid.Use(as.Webhook, mid.RequirePermission(models.PermissionModifySystem)))
|
||||||
as.handler = router
|
as.handler = router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
log "github.com/gophish/gophish/logger"
|
||||||
|
"github.com/gophish/gophish/models"
|
||||||
|
"github.com/gophish/gophish/webhook"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Webhooks returns a list of webhooks, both active and disabled
|
||||||
|
func (as *Server) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
whs, err := models.GetWebhooks()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, whs, http.StatusOK)
|
||||||
|
|
||||||
|
case r.Method == "POST":
|
||||||
|
wh := models.Webhook{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&wh)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = models.PostWebhook(&wh)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, wh, http.StatusCreated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook returns details of a single webhook specified by "id" parameter
|
||||||
|
func (as *Server) Webhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
|
wh, err := models.GetWebhook(id)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Webhook not found"}, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
JSONResponse(w, wh, http.StatusOK)
|
||||||
|
|
||||||
|
case r.Method == "DELETE":
|
||||||
|
err = models.DeleteWebhook(id)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("Deleted webhook with id: %d", id)
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "Webhook deleted Successfully!"}, http.StatusOK)
|
||||||
|
|
||||||
|
case r.Method == "PUT":
|
||||||
|
wh2 := models.Webhook{}
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&wh2)
|
||||||
|
wh2.Id = id
|
||||||
|
err = models.PutWebhook(&wh2)
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, wh2, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWebhook makes an HTTP request to a specified remote url to ensure that it's valid.
|
||||||
|
func (as *Server) ValidateWebhook(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case r.Method == "POST":
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
|
wh, err := models.GetWebhook(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = webhook.Send(webhook.EndPoint{URL: wh.URL, Secret: wh.Secret}, "")
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, wh, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,6 +110,7 @@ func (as *AdminServer) registerRoutes() {
|
||||||
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
|
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
|
||||||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||||
|
router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||||
// Create the API routes
|
// Create the API routes
|
||||||
api := api.NewServer(api.WithWorker(as.worker))
|
api := api.NewServer(api.WithWorker(as.worker))
|
||||||
router.PathPrefix("/api/").Handler(api)
|
router.PathPrefix("/api/").Handler(api)
|
||||||
|
@ -238,6 +239,13 @@ func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
||||||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Webhooks is an admin-only handler that handles webhooks
|
||||||
|
func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := newTemplateParams(r)
|
||||||
|
params.Title = "Webhooks"
|
||||||
|
getTemplate(w, "webhooks").ExecuteTemplate(w, "base", params)
|
||||||
|
}
|
||||||
|
|
||||||
// Login handles the authentication flow for a user. If credentials are valid,
|
// Login handles the authentication flow for a user. If credentials are valid,
|
||||||
// a session is created
|
// a session is created
|
||||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
CREATE TABLE IF NOT EXISTS "webhooks" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"name" varchar(255),
|
||||||
|
"url" varchar(1000),
|
||||||
|
"secret" varchar(255),
|
||||||
|
"is_active" boolean default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
-- SQL in section 'Up' is executed when this migration is applied
|
||||||
|
CREATE TABLE IF NOT EXISTS "webhooks" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"name" varchar(255),
|
||||||
|
"url" varchar(1000),
|
||||||
|
"secret" varchar(255),
|
||||||
|
"is_active" boolean default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- SQL section 'Down' is executed when this migration is rolled back
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/gophish/gophish/logger"
|
log "github.com/gophish/gophish/logger"
|
||||||
|
"github.com/gophish/gophish/webhook"
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -157,6 +158,21 @@ func (c *Campaign) UpdateStatus(s string) error {
|
||||||
func (c *Campaign) AddEvent(e *Event) error {
|
func (c *Campaign) AddEvent(e *Event) error {
|
||||||
e.CampaignId = c.Id
|
e.CampaignId = c.Id
|
||||||
e.Time = time.Now().UTC()
|
e.Time = time.Now().UTC()
|
||||||
|
|
||||||
|
whs, err := GetActiveWebhooks()
|
||||||
|
if err == nil {
|
||||||
|
whEndPoints := []webhook.EndPoint{}
|
||||||
|
for _, wh := range whs {
|
||||||
|
whEndPoints = append(whEndPoints, webhook.EndPoint{
|
||||||
|
URL: wh.URL,
|
||||||
|
Secret: wh.Secret,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
webhook.SendAll(whEndPoints, e)
|
||||||
|
} else {
|
||||||
|
log.Errorf("error getting active webhooks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return db.Save(e).Error
|
return db.Save(e).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/liamstask/goose/lib/goose"
|
"bitbucket.org/liamstask/goose/lib/goose"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
log "github.com/gophish/gophish/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Webhook represents the webhook model
|
||||||
|
type Webhook struct {
|
||||||
|
Id int64 `json:"id" gorm:"column:id; primary_key:yes"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrURLNotSpecified indicates there was no URL specified
|
||||||
|
var ErrURLNotSpecified = errors.New("URL can't be empty")
|
||||||
|
|
||||||
|
// ErrNameNotSpecified indicates there was no name specified
|
||||||
|
var ErrNameNotSpecified = errors.New("Name can't be empty")
|
||||||
|
|
||||||
|
// GetWebhooks returns the webhooks
|
||||||
|
func GetWebhooks() ([]Webhook, error) {
|
||||||
|
whs := []Webhook{}
|
||||||
|
err := db.Find(&whs).Error
|
||||||
|
return whs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActiveWebhooks returns the active webhooks
|
||||||
|
func GetActiveWebhooks() ([]Webhook, error) {
|
||||||
|
whs := []Webhook{}
|
||||||
|
err := db.Where("is_active=?", true).Find(&whs).Error
|
||||||
|
return whs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWebhook returns the webhook that the given id corresponds to.
|
||||||
|
// If no webhook is found, an error is returned.
|
||||||
|
func GetWebhook(id int64) (Webhook, error) {
|
||||||
|
wh := Webhook{}
|
||||||
|
err := db.Where("id=?", id).First(&wh).Error
|
||||||
|
return wh, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostWebhook creates a new webhook in the database.
|
||||||
|
func PostWebhook(wh *Webhook) error {
|
||||||
|
err := wh.Validate()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Save(wh).Error
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutWebhook edits an existing webhook in the database.
|
||||||
|
func PutWebhook(wh *Webhook) error {
|
||||||
|
err := wh.Validate()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Save(wh).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWebhook deletes an existing webhook in the database.
|
||||||
|
// An error is returned if a webhook with the given id isn't found.
|
||||||
|
func DeleteWebhook(id int64) error {
|
||||||
|
err := db.Where("id=?", id).Delete(&Webhook{}).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wh *Webhook) Validate() error {
|
||||||
|
if wh.URL == "" {
|
||||||
|
return ErrURLNotSpecified
|
||||||
|
}
|
||||||
|
if wh.Name == "" {
|
||||||
|
return ErrNameNotSpecified
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -223,6 +223,28 @@ var api = {
|
||||||
return query("/users/" + id, "DELETE", {}, true)
|
return query("/users/" + id, "DELETE", {}, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
webhooks: {
|
||||||
|
get: function() {
|
||||||
|
return query("/webhooks/", "GET", {}, false)
|
||||||
|
},
|
||||||
|
post: function(webhook) {
|
||||||
|
return query("/webhooks/", "POST", webhook, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
webhookId: {
|
||||||
|
get: function(id) {
|
||||||
|
return query("/webhooks/" + id, "GET", {}, false)
|
||||||
|
},
|
||||||
|
put: function(webhook) {
|
||||||
|
return query("/webhooks/" + webhook.id, "PUT", webhook, true)
|
||||||
|
},
|
||||||
|
delete: function(id) {
|
||||||
|
return query("/webhooks/" + id, "DELETE", {}, false)
|
||||||
|
},
|
||||||
|
ping: function(id) {
|
||||||
|
return query("/webhooks/" + id + "/validate", "POST", {}, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
// import handles all of the "import" functions in the api
|
// import handles all of the "import" functions in the api
|
||||||
import_email: function (req) {
|
import_email: function (req) {
|
||||||
return query("/import/email", "POST", req, false)
|
return query("/import/email", "POST", req, false)
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
let webhooks = [];
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
$("#name").val("");
|
||||||
|
$("#url").val("");
|
||||||
|
$("#secret").val("");
|
||||||
|
$("#is_active").prop("checked", false);
|
||||||
|
$("#flashes").empty();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveWebhook = (id) => {
|
||||||
|
let wh = {
|
||||||
|
name: $("#name").val(),
|
||||||
|
url: $("#url").val(),
|
||||||
|
secret: $("#secret").val(),
|
||||||
|
is_active: $("#is_active").is(":checked"),
|
||||||
|
};
|
||||||
|
if (id != -1) {
|
||||||
|
wh.id = id;
|
||||||
|
api.webhookId.put(wh)
|
||||||
|
.success(function(data) {
|
||||||
|
dismiss();
|
||||||
|
load();
|
||||||
|
$("#modal").modal("hide");
|
||||||
|
successFlash(`Webhook "${escape(wh.name)}" has been updated successfully!`);
|
||||||
|
})
|
||||||
|
.error(function(data) {
|
||||||
|
modalError(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
api.webhooks.post(wh)
|
||||||
|
.success(function(data) {
|
||||||
|
load();
|
||||||
|
dismiss();
|
||||||
|
$("#modal").modal("hide");
|
||||||
|
successFlash(`Webhook "${escape(wh.name)}" has been created successfully!`);
|
||||||
|
})
|
||||||
|
.error(function(data) {
|
||||||
|
modalError(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
$("#webhookTable").hide();
|
||||||
|
$("#loading").show();
|
||||||
|
api.webhooks.get()
|
||||||
|
.success((whs) => {
|
||||||
|
webhooks = whs;
|
||||||
|
$("#loading").hide()
|
||||||
|
$("#webhookTable").show()
|
||||||
|
let webhookTable = $("#webhookTable").DataTable({
|
||||||
|
destroy: true,
|
||||||
|
columnDefs: [{
|
||||||
|
orderable: false,
|
||||||
|
targets: "no-sort"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
webhookTable.clear();
|
||||||
|
$.each(webhooks, (i, webhook) => {
|
||||||
|
webhookTable.row.add([
|
||||||
|
escapeHtml(webhook.name),
|
||||||
|
escapeHtml(webhook.url),
|
||||||
|
escapeHtml(webhook.is_active),
|
||||||
|
`
|
||||||
|
<div class="pull-right">
|
||||||
|
<button class="btn btn-primary ping_button" data-webhook-id="${webhook.id}">
|
||||||
|
Ping
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="${webhook.id}">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger delete_button" data-webhook-id="${webhook.id}">
|
||||||
|
<i class="fa fa-trash-o"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
]).draw()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.error(() => {
|
||||||
|
errorFlash("Error fetching webhooks")
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const editWebhook = (id) => {
|
||||||
|
$("#modalSubmit").unbind("click").click(() => {
|
||||||
|
saveWebhook(id);
|
||||||
|
});
|
||||||
|
if (id !== -1) {
|
||||||
|
api.webhookId.get(id)
|
||||||
|
.success(function(wh) {
|
||||||
|
$("#name").val(wh.name);
|
||||||
|
$("#url").val(wh.url);
|
||||||
|
$("#secret").val(wh.secret);
|
||||||
|
$("#is_active").prop("checked", wh.is_active);
|
||||||
|
})
|
||||||
|
.error(function () {
|
||||||
|
errorFlash("Error fetching webhook")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteWebhook = (id) => {
|
||||||
|
var wh = webhooks.find(x => x.id == id);
|
||||||
|
if (!wh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: `This will delete the webhook '${escape(wh.name)}'`,
|
||||||
|
type: "warning",
|
||||||
|
animation: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Delete",
|
||||||
|
confirmButtonColor: "#428bca",
|
||||||
|
reverseButtons: true,
|
||||||
|
allowOutsideClick: false,
|
||||||
|
preConfirm: function () {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.webhookId.delete(id)
|
||||||
|
.success((msg) => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.error((data) => {
|
||||||
|
reject(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
Swal.showValidationMessage(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).then(function(result) {
|
||||||
|
if (result.value) {
|
||||||
|
Swal.fire(
|
||||||
|
"Webhook Deleted!",
|
||||||
|
`The webhook has been deleted!`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$("button:contains('OK')").on("click", function() {
|
||||||
|
location.reload();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const pingUrl = (btn, whId) => {
|
||||||
|
dismiss();
|
||||||
|
btn.disabled = true;
|
||||||
|
api.webhookId.ping(whId)
|
||||||
|
.success(function(wh) {
|
||||||
|
btn.disabled = false;
|
||||||
|
successFlash(`Ping of "${escape(wh.name)}" webhook succeeded.`);
|
||||||
|
})
|
||||||
|
.error(function(data) {
|
||||||
|
btn.disabled = false;
|
||||||
|
var wh = webhooks.find(x => x.id == whId);
|
||||||
|
if (!wh) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorFlash(`Ping of "${escape(wh.name)}" webhook failed: "${data.responseJSON.message}"`)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
load();
|
||||||
|
$("#modal").on("hide.bs.modal", function() {
|
||||||
|
dismiss();
|
||||||
|
});
|
||||||
|
$("#new_button").on("click", function() {
|
||||||
|
editWebhook(-1);
|
||||||
|
});
|
||||||
|
$("#webhookTable").on("click", ".edit_button", function(e) {
|
||||||
|
editWebhook($(this).attr("data-webhook-id"));
|
||||||
|
});
|
||||||
|
$("#webhookTable").on("click", ".delete_button", function(e) {
|
||||||
|
deleteWebhook($(this).attr("data-webhook-id"));
|
||||||
|
});
|
||||||
|
$("#webhookTable").on("click", ".ping_button", function(e) {
|
||||||
|
pingUrl(e.currentTarget, e.currentTarget.dataset.webhookId);
|
||||||
|
});
|
||||||
|
});
|
|
@ -27,6 +27,9 @@
|
||||||
<li>
|
<li>
|
||||||
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/webhooks">Webhooks<span class="nav-badge badge pull-right">Admin</span></a>
|
||||||
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
<li>
|
<li>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||||
|
<h1 class="page-header">
|
||||||
|
{{.Title}}
|
||||||
|
</h1>
|
||||||
|
<div id="flashes" class="row"></div>
|
||||||
|
<div class="row">
|
||||||
|
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||||
|
data-webhook-id="-1" data-target="#modal">
|
||||||
|
<i class="fa fa-plus"></i> New Webhook</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<table id="webhookTable" class="table" style="display:none;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-md-2 no-sort">Title</th>
|
||||||
|
<th class="col-md-2 no-sort">Url</th>
|
||||||
|
<th class="col-md-2 no-">Is active</th>
|
||||||
|
<th class="col-md-2 no-sort"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title" id="groupModalLabel">Add or Edit Webhook</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="modal_body">
|
||||||
|
<div class="row" id="modal.flashes"></div>
|
||||||
|
<label class="control-label" for="title">Name:</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="Name" id="name" required autofocus />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="control-label" for="url">URL:</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="https://example.com/webhook1" id="url" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="control-label" for="secret">Secret:</label>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="Secret" id="secret" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox checkbox-primary">
|
||||||
|
<input type="checkbox" id="is_active" value="true" />
|
||||||
|
<label for="is_active">Is active <i class="fa fa-question-circle"
|
||||||
|
data-toggle="tooltip" data-placement="right"
|
||||||
|
title="Data is sent only to the active webhooks"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{end}} {{define "scripts"}}
|
||||||
|
<!-- TODO replace with "min" -->
|
||||||
|
<script src="/js/src/app/webhooks.js"></script>
|
||||||
|
{{end}}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
gophish - Open-Source Phishing Framework
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Jordan Wright
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package webhook contains the functionality for handling outcoming webhooks.
|
||||||
|
package webhook
|
|
@ -0,0 +1,105 @@
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/gophish/gophish/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
|
||||||
|
// DefaultTimeoutSeconds is amount of seconds of timeout used by HTTP sender
|
||||||
|
DefaultTimeoutSeconds = 10
|
||||||
|
|
||||||
|
// MinHTTPStatusErrorCode is the lowest number of an HTTP response which indicates an error
|
||||||
|
MinHTTPStatusErrorCode = 400
|
||||||
|
|
||||||
|
// SignatureHeader is the name of an HTTP header used to which contains signature of a webhook
|
||||||
|
SignatureHeader = "X-Gophish-Signature"
|
||||||
|
|
||||||
|
// Sha256Prefix is the prefix that specifies the hashing algorithm used for signature
|
||||||
|
Sha256Prefix = "sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sender defines behaviour of an entity by which webhook is sent
|
||||||
|
type Sender interface {
|
||||||
|
Send(endPoint EndPoint, data interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultSender struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var senderInstance = &defaultSender{
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: time.Second * DefaultTimeoutSeconds,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndPoint represents and end point to send a webhook to: url and secret by which payload is signed
|
||||||
|
type EndPoint struct {
|
||||||
|
URL string
|
||||||
|
Secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends data to a single EndPoint
|
||||||
|
func Send(endPoint EndPoint, data interface{}) error {
|
||||||
|
return senderInstance.Send(endPoint, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendAll sends data to each of the EndPoints
|
||||||
|
func SendAll(endPoints []EndPoint, data interface{}) {
|
||||||
|
for _, ept := range endPoints {
|
||||||
|
go func(ept1 EndPoint) {
|
||||||
|
senderInstance.Send(ept1, data)
|
||||||
|
}(EndPoint{URL: ept.URL, Secret: ept.Secret})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send contains the implementation of sending webhook to an EndPoint
|
||||||
|
func (ds defaultSender) Send(endPoint EndPoint, data interface{}) error {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", endPoint.URL, bytes.NewBuffer(jsonData))
|
||||||
|
signat, err := sign(endPoint.Secret, jsonData)
|
||||||
|
req.Header.Set(SignatureHeader, fmt.Sprintf("%s=%s", Sha256Prefix, signat))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := ds.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= MinHTTPStatusErrorCode {
|
||||||
|
errMsg := fmt.Sprintf("http status of response: %s", resp.Status)
|
||||||
|
log.Error(errMsg)
|
||||||
|
return errors.New(errMsg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(secret string, data []byte) (string, error) {
|
||||||
|
hash1 := hmac.New(sha256.New, []byte(secret))
|
||||||
|
_, err := hash1.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hexStr := hex.EncodeToString(hash1.Sum(nil))
|
||||||
|
return hexStr, nil
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"log"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockSender struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockSender() *mockSender {
|
||||||
|
ms := &mockSender {
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ms mockSender) Send(endPoint EndPoint, data interface{}) error {
|
||||||
|
log.Println("[test] mocked 'Send' function")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebhookSuite) TestSendMocked() {
|
||||||
|
mcSnd := newMockSender()
|
||||||
|
endp1 := EndPoint{URL: "http://example.com/a1", Secret: "s1"}
|
||||||
|
d1 := map[string]string {
|
||||||
|
"a1": "a11",
|
||||||
|
"a2": "a22",
|
||||||
|
"a3": "a33",
|
||||||
|
}
|
||||||
|
err := mcSnd.Send(endp1, d1)
|
||||||
|
s.Nil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (s *WebhookSuite) TestSendReal() {
|
||||||
|
expectedSign := "004b36ca3fcbc01a08b17bf5d4a7e1aa0b10e14f55f3f8bd9acac0c7e8d2635d"
|
||||||
|
secret := "secret456"
|
||||||
|
d1 := map[string]interface{} {
|
||||||
|
"key1": "val1",
|
||||||
|
"key2": "val2",
|
||||||
|
"key3": "val3",
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("[test] running the server...")
|
||||||
|
|
||||||
|
signStartIdx := len(Sha256Prefix) + 1
|
||||||
|
realSignRaw := r.Header.Get(SignatureHeader)
|
||||||
|
realSign := realSignRaw[signStartIdx:]
|
||||||
|
assert.Equal(s.T(), expectedSign, realSign)
|
||||||
|
|
||||||
|
contTypeJsonHeader := r.Header.Get("Content-Type")
|
||||||
|
assert.Equal(s.T(), contTypeJsonHeader, "application/json")
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
s.Nil(err)
|
||||||
|
|
||||||
|
var d2 map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &d2)
|
||||||
|
s.Nil(err)
|
||||||
|
assert.Equal(s.T(), d1, d2)
|
||||||
|
}))
|
||||||
|
|
||||||
|
defer ts.Close()
|
||||||
|
endp1 := EndPoint{URL: ts.URL, Secret: secret}
|
||||||
|
err := Send(endp1, d1)
|
||||||
|
s.Nil(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebhookSuite) TestSignature() {
|
||||||
|
secret := "secret123"
|
||||||
|
payload := []byte("some payload456")
|
||||||
|
expectedSign := "ab7844c1e9149f8dc976c4188a72163c005930f3c2266a163ffe434230bdf761"
|
||||||
|
realSign, err := sign(secret, payload)
|
||||||
|
s.Nil(err)
|
||||||
|
assert.Equal(s.T(), expectedSign, realSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(WebhookSuite))
|
||||||
|
}
|
Loading…
Reference in New Issue