mirror of
https://github.com/gophish/gophish
synced 2024-11-14 00:07:19 +00:00
Add Webhook Support
Adds support for managing outgoing webhooks. Closes #1602
This commit is contained in:
parent
699532f256
commit
28cd7a238e
17 changed files with 758 additions and 4 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,6 +13,7 @@ node_modules
|
|||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
.DS_Store
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
|
|
BIN
controllers/.DS_Store
vendored
Normal file
BIN
controllers/.DS_Store
vendored
Normal file
Binary file not shown.
|
@ -71,6 +71,9 @@ func (as *Server) registerRoutes() {
|
|||
router.HandleFunc("/import/group", as.ImportGroup)
|
||||
router.HandleFunc("/import/email", as.ImportEmail)
|
||||
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
|
||||
}
|
||||
|
||||
|
|
96
controllers/api/webhook.go
Normal file
96
controllers/api/webhook.go
Normal file
|
@ -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("/settings", mid.Use(as.Settings, 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
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// a session is created
|
||||
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"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/webhook"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -157,6 +158,21 @@ func (c *Campaign) UpdateStatus(s string) error {
|
|||
func (c *Campaign) AddEvent(e *Event) error {
|
||||
e.CampaignId = c.Id
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ package models
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"bitbucket.org/liamstask/goose/lib/goose"
|
||||
|
||||
|
|
86
models/webhook.go
Normal file
86
models/webhook.go
Normal file
|
@ -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)
|
||||
}
|
||||
},
|
||||
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_email: function (req) {
|
||||
return query("/import/email", "POST", req, false)
|
||||
|
|
182
static/js/src/app/webhooks.js
Normal file
182
static/js/src/app/webhooks.js
Normal file
|
@ -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>
|
||||
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/webhooks">Webhooks<span class="nav-badge badge pull-right">Admin</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<hr>
|
||||
|
|
79
templates/webhooks.html
Normal file
79
templates/webhooks.html
Normal file
|
@ -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}}
|
28
webhook/doc.go
Normal file
28
webhook/doc.go
Normal file
|
@ -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
|
105
webhook/webhook.go
Normal file
105
webhook/webhook.go
Normal file
|
@ -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
|
||||
}
|
95
webhook/webhook_test.go
Normal file
95
webhook/webhook_test.go
Normal file
|
@ -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 a new issue