Adding the ability to schedule campaigns. Fixes #21

pull/291/head
Jordan Wright 2016-06-07 21:42:09 -05:00
parent a8aac75f99
commit 082023aae0
9 changed files with 3010 additions and 37 deletions

View File

@ -83,7 +83,6 @@ func API_Campaigns(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest) JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return return
} }
Worker.Queue <- &c
JSONResponse(w, c, http.StatusCreated) JSONResponse(w, c, http.StatusCreated)
} }
} }

View File

@ -0,0 +1,9 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE "campaigns" ADD COLUMN "launch_date" DATETIME;
UPDATE "campaigns" SET "launch_date" = "created_date";
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View File

@ -2,7 +2,6 @@ package models
import ( import (
"errors" "errors"
"fmt"
"time" "time"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
@ -14,6 +13,7 @@ type Campaign struct {
UserId int64 `json:"-"` UserId int64 `json:"-"`
Name string `json:"name" sql:"not null"` Name string `json:"name" sql:"not null"`
CreatedDate time.Time `json:"created_date"` CreatedDate time.Time `json:"created_date"`
LaunchDate time.Time `json:"launch_date"`
CompletedDate time.Time `json:"completed_date"` CompletedDate time.Time `json:"completed_date"`
TemplateId int64 `json:"-"` TemplateId int64 `json:"-"`
Template Template `json:"template"` Template Template `json:"template"`
@ -139,7 +139,7 @@ func (c *Campaign) getDetails() error {
c.Page = Page{Name: "[Deleted]"} c.Page = Page{Name: "[Deleted]"}
Logger.Printf("%s: page not found for campaign\n", err) Logger.Printf("%s: page not found for campaign\n", err)
} }
err = db.Table("SMTP").Where("id=?", c.SMTPId).Find(&c.SMTP).Error err = db.Table("smtp").Where("id=?", c.SMTPId).Find(&c.SMTP).Error
if err != nil { if err != nil {
// Check if the SMTP was deleted // Check if the SMTP was deleted
if err != gorm.ErrRecordNotFound { if err != gorm.ErrRecordNotFound {
@ -167,7 +167,7 @@ func GetCampaigns(uid int64) ([]Campaign, error) {
cs := []Campaign{} cs := []Campaign{}
err := db.Model(&User{Id: uid}).Related(&cs).Error err := db.Model(&User{Id: uid}).Related(&cs).Error
if err != nil { if err != nil {
fmt.Println(err) Logger.Println(err)
} }
for i, _ := range cs { for i, _ := range cs {
err = cs[i].getDetails() err = cs[i].getDetails()
@ -190,6 +190,24 @@ func GetCampaign(id int64, uid int64) (Campaign, error) {
return c, err return c, err
} }
// GetQueuedCampaigns returns the campaigns that are queued up for this given minute
func GetQueuedCampaigns(t time.Time) ([]Campaign, error) {
cs := []Campaign{}
err := db.Where("launch_date <= ?", t).
Where("status = ?", CAMPAIGN_QUEUED).Find(&cs).Error
if err != nil {
Logger.Println(err)
}
Logger.Printf("Found %d Campaigns to run\n", len(cs))
for i, _ := range cs {
err = cs[i].getDetails()
if err != nil {
Logger.Println(err)
}
}
return cs, err
}
// PostCampaign inserts a campaign and all associated records into the database. // PostCampaign inserts a campaign and all associated records into the database.
func PostCampaign(c *Campaign, uid int64) error { func PostCampaign(c *Campaign, uid int64) error {
if err := c.Validate(); err != nil { if err := c.Validate(); err != nil {
@ -200,6 +218,9 @@ func PostCampaign(c *Campaign, uid int64) error {
c.CreatedDate = time.Now() c.CreatedDate = time.Now()
c.CompletedDate = time.Time{} c.CompletedDate = time.Time{}
c.Status = CAMPAIGN_QUEUED c.Status = CAMPAIGN_QUEUED
if c.LaunchDate.IsZero() {
c.LaunchDate = time.Now()
}
// Check to make sure all the groups already exist // Check to make sure all the groups already exist
for i, g := range c.Groups { for i, g := range c.Groups {
c.Groups[i], err = GetGroupByName(g.Name, uid) c.Groups[i], err = GetGroupByName(g.Name, uid)

373
static/css/bootstrap-datetime.css vendored Normal file
View File

@ -0,0 +1,373 @@
/*!
* Datetimepicker for Bootstrap 3
* version : 4.15.35
* https://github.com/Eonasdan/bootstrap-datetimepicker/
*/
.bootstrap-datetimepicker-widget {
list-style: none;
}
.bootstrap-datetimepicker-widget.dropdown-menu {
margin: 2px 0;
padding: 4px;
width: 19em;
}
@media (min-width: 768px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 992px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
@media (min-width: 1200px) {
.bootstrap-datetimepicker-widget.dropdown-menu.timepicker-sbs {
width: 38em;
}
}
.bootstrap-datetimepicker-widget.dropdown-menu:before,
.bootstrap-datetimepicker-widget.dropdown-menu:after {
content: '';
display: inline-block;
position: absolute;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #cccccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
top: -7px;
left: 7px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
top: -6px;
left: 8px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.top:before {
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 7px solid #cccccc;
border-top-color: rgba(0, 0, 0, 0.2);
bottom: -7px;
left: 6px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.top:after {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid white;
bottom: -6px;
left: 7px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:before {
left: auto;
right: 6px;
}
.bootstrap-datetimepicker-widget.dropdown-menu.pull-right:after {
left: auto;
right: 7px;
}
.bootstrap-datetimepicker-widget .list-unstyled {
margin: 0;
}
.bootstrap-datetimepicker-widget a[data-action] {
padding: 6px 0;
}
.bootstrap-datetimepicker-widget a[data-action]:active {
box-shadow: none;
}
.bootstrap-datetimepicker-widget .timepicker-hour,
.bootstrap-datetimepicker-widget .timepicker-minute,
.bootstrap-datetimepicker-widget .timepicker-second {
width: 54px;
font-weight: bold;
font-size: 1.2em;
margin: 0;
}
.bootstrap-datetimepicker-widget button[data-action] {
padding: 6px;
}
.bootstrap-datetimepicker-widget .btn[data-action="incrementHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Increment Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="incrementMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Increment Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="decrementHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Decrement Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="decrementMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Decrement Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="showHours"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Show Hours";
}
.bootstrap-datetimepicker-widget .btn[data-action="showMinutes"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Show Minutes";
}
.bootstrap-datetimepicker-widget .btn[data-action="togglePeriod"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Toggle AM/PM";
}
.bootstrap-datetimepicker-widget .btn[data-action="clear"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Clear the picker";
}
.bootstrap-datetimepicker-widget .btn[data-action="today"]::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Set the date to today";
}
.bootstrap-datetimepicker-widget .picker-switch {
text-align: center;
}
.bootstrap-datetimepicker-widget .picker-switch::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Toggle Date and Time Screens";
}
.bootstrap-datetimepicker-widget .picker-switch td {
padding: 0;
margin: 0;
height: auto;
width: auto;
line-height: inherit;
}
.bootstrap-datetimepicker-widget .picker-switch td span {
line-height: 2.5;
height: 2.5em;
width: 100%;
}
.bootstrap-datetimepicker-widget table {
width: 100%;
margin: 0;
}
.bootstrap-datetimepicker-widget table td,
.bootstrap-datetimepicker-widget table th {
text-align: center;
border-radius: 4px;
}
.bootstrap-datetimepicker-widget table th {
height: 20px;
line-height: 20px;
width: 20px;
}
.bootstrap-datetimepicker-widget table th.picker-switch {
width: 145px;
}
.bootstrap-datetimepicker-widget table th.disabled,
.bootstrap-datetimepicker-widget table th.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget table th.prev::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Previous Month";
}
.bootstrap-datetimepicker-widget table th.next::after {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
content: "Next Month";
}
.bootstrap-datetimepicker-widget table thead tr:first-child th {
cursor: pointer;
}
.bootstrap-datetimepicker-widget table thead tr:first-child th:hover {
background: #eeeeee;
}
.bootstrap-datetimepicker-widget table td {
height: 54px;
line-height: 54px;
width: 54px;
}
.bootstrap-datetimepicker-widget table td.cw {
font-size: .8em;
height: 20px;
line-height: 20px;
color: #777777;
}
.bootstrap-datetimepicker-widget table td.day {
height: 20px;
line-height: 20px;
width: 20px;
}
.bootstrap-datetimepicker-widget table td.day:hover,
.bootstrap-datetimepicker-widget table td.hour:hover,
.bootstrap-datetimepicker-widget table td.minute:hover,
.bootstrap-datetimepicker-widget table td.second:hover {
background: #eeeeee;
cursor: pointer;
}
.bootstrap-datetimepicker-widget table td.old,
.bootstrap-datetimepicker-widget table td.new {
color: #777777;
}
.bootstrap-datetimepicker-widget table td.today {
position: relative;
}
.bootstrap-datetimepicker-widget table td.today:before {
content: '';
display: inline-block;
border: solid transparent;
border-width: 0 0 7px 7px;
border-bottom-color: #337ab7;
border-top-color: rgba(0, 0, 0, 0.2);
position: absolute;
bottom: 4px;
right: 4px;
}
.bootstrap-datetimepicker-widget table td.active,
.bootstrap-datetimepicker-widget table td.active:hover {
background-color: #337ab7;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.bootstrap-datetimepicker-widget table td.active.today:before {
border-bottom-color: #fff;
}
.bootstrap-datetimepicker-widget table td.disabled,
.bootstrap-datetimepicker-widget table td.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget table td span {
display: inline-block;
width: 54px;
height: 54px;
line-height: 54px;
margin: 2px 1.5px;
cursor: pointer;
border-radius: 4px;
}
.bootstrap-datetimepicker-widget table td span:hover {
background: #eeeeee;
}
.bootstrap-datetimepicker-widget table td span.active {
background-color: #337ab7;
color: #ffffff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.bootstrap-datetimepicker-widget table td span.old {
color: #777777;
}
.bootstrap-datetimepicker-widget table td span.disabled,
.bootstrap-datetimepicker-widget table td span.disabled:hover {
background: none;
color: #777777;
cursor: not-allowed;
}
.bootstrap-datetimepicker-widget.usetwentyfour td.hour {
height: 27px;
line-height: 27px;
}
.bootstrap-datetimepicker-widget.wider {
width: 21em;
}
.bootstrap-datetimepicker-widget .datepicker-decades .decade {
line-height: 1.8em !important;
}
.input-group.date .input-group-addon {
cursor: pointer;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}

View File

@ -31,8 +31,9 @@ function launch() {
name: $("#page").val() name: $("#page").val()
}, },
smtp: { smtp: {
name: $("#profile").val() name: $("#profile").val()
}, },
launch_date: moment($("#launch_date").val(), "MM/DD/YYYY HH:mm").format(),
groups: groups groups: groups
} }
launchHtml = $("launchButton").html() launchHtml = $("launchButton").html()
@ -66,7 +67,7 @@ function sendTestEmail() {
name: $("#page").val() name: $("#page").val()
}, },
smtp: { smtp: {
name: $("#profile").val() name: $("#profile").val()
} }
} }
btnHtml = $("#sendTestModalSubmit").html() btnHtml = $("#sendTestModalSubmit").html()
@ -141,15 +142,15 @@ function edit(campaign) {
page_bh.add(pages) page_bh.add(pages)
} }
}) })
api.SMTP.get() api.SMTP.get()
.success(function(profiles) { .success(function(profiles) {
if (profiles.length == 0){ if (profiles.length == 0) {
modalError("No profiles found!") modalError("No profiles found!")
return false return false
} else { } else {
profile_bh.add(profiles) profile_bh.add(profiles)
} }
}) })
} }
} }
@ -201,22 +202,29 @@ function copy(idx) {
$("#page").val(campaign.page.name) $("#page").val(campaign.page.name)
$("#profile").val(campaign.smtp.name) $("#profile").val(campaign.smtp.name)
$("#url").val(campaign.url) $("#url").val(campaign.url)
$.each(campaign.groups, function(i, group){ $.each(campaign.groups, function(i, group) {
groupTable.row.add([ groupTable.row.add([
group.name, group.name,
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>' '<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
]).draw() ]).draw()
$("#groupTable").on("click", "span>i.fa-trash-o", function() { $("#groupTable").on("click", "span>i.fa-trash-o", function() {
groupTable.row($(this).parents('tr')) groupTable.row($(this).parents('tr'))
.remove() .remove()
.draw(); .draw();
}) })
}) })
} }
$(document).ready(function() { $(document).ready(function() {
// Setup multiple modals $("#launch_date").datetimepicker({
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html "widgetPositioning": {
"vertical": "bottom"
},
"showTodayButton": true,
"defaultDate": moment()
})
// Setup multiple modals
// Code based on http://miles-by-motorcycle.com/static/bootstrap-modal/index.html
$('.modal').on('hidden.bs.modal', function(event) { $('.modal').on('hidden.bs.modal', function(event) {
$(this).removeClass('fv-modal-stack'); $(this).removeClass('fv-modal-stack');
$('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1); $('body').data('fv_open_modals', $('body').data('fv_open_modals') - 1);
@ -253,7 +261,7 @@ $(document).ready(function() {
}, this)); }, this));
}; };
$('#modal').on('hidden.bs.modal', function(event) { $('#modal').on('hidden.bs.modal', function(event) {
dismiss() dismiss()
}); });
api.campaigns.get() api.campaigns.get()
.success(function(cs) { .success(function(cs) {

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
<link href="/css/dataTables.bootstrap.css" rel="stylesheet"> <link href="/css/dataTables.bootstrap.css" rel="stylesheet">
<link href="/css/font-awesome.min.css" rel="stylesheet"> <link href="/css/font-awesome.min.css" rel="stylesheet">
<link href="/css/chartist.min.css" rel="stylesheet"> <link href="/css/chartist.min.css" rel="stylesheet">
<link href="/css/bootstrap-datetime.css" rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Roboto:700,500' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Roboto:700,500' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
<link href="/css/checkbox.css" rel="stylesheet"> <link href="/css/checkbox.css" rel="stylesheet">

View File

@ -81,6 +81,8 @@
<br> <br>
<label class="control-label" for="url">URL: <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Location of gophish listener (must be reachable by targets!)"></i></label> <label class="control-label" for="url">URL: <i class="fa fa-question-circle" data-toggle="tooltip" data-placement="right" title="Location of gophish listener (must be reachable by targets!)"></i></label>
<input type="text" class="form-control" placeholder="http://192.168.1.1" id="url"/> <input type="text" class="form-control" placeholder="http://192.168.1.1" id="url"/>
<label class="control-label" for="url">Schedule: </label>
<input type="text" class="form-control" id="launch_date"/>
<label class="control-label" for="profile">Sending Profile:</label> <label class="control-label" for="profile">Sending Profile:</label>
<div class="input-group"> <div class="input-group">
<input type="text" class="typeahead form-control" placeholder="Sending Profile" id="profile"/> <input type="text" class="typeahead form-control" placeholder="Sending Profile" id="profile"/>
@ -153,6 +155,7 @@
</div> </div>
{{end}} {{end}}
{{define "scripts"}} {{define "scripts"}}
<script src="/js/bootstrap-datetime.js"></script>
<script src="/js/hogan.js"></script> <script src="/js/hogan.js"></script>
<script src="/js/typeahead.min.js"></script> <script src="/js/typeahead.min.js"></script>
<script src="/js/app/campaigns.js"></script> <script src="/js/app/campaigns.js"></script>

View File

@ -13,6 +13,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/gophish/gophish/models" "github.com/gophish/gophish/models"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
@ -22,23 +23,29 @@ import (
var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile) var Logger = log.New(os.Stdout, " ", log.Ldate|log.Ltime|log.Lshortfile)
// Worker is the background worker that handles watching for new campaigns and sending emails appropriately. // Worker is the background worker that handles watching for new campaigns and sending emails appropriately.
type Worker struct { type Worker struct{}
Queue chan *models.Campaign
}
// New creates a new worker object to handle the creation of campaigns // New creates a new worker object to handle the creation of campaigns
func New() *Worker { func New() *Worker {
return &Worker{ return &Worker{}
Queue: make(chan *models.Campaign),
}
} }
// Start launches the worker to monitor the database for any jobs. // Start launches the worker to poll the database every minute for any jobs.
// If a job is found, it launches the job // If a job is found, it launches the job
func (w *Worker) Start() { func (w *Worker) Start() {
Logger.Println("Background Worker Started Successfully - Waiting for Campaigns") Logger.Println("Background Worker Started Successfully - Waiting for Campaigns")
for { for t := range time.Tick(1 * time.Minute) {
processCampaign(<-w.Queue) cs, err := models.GetQueuedCampaigns(t)
// Not really sure of a clean way to catch errors per campaign...
if err != nil {
Logger.Println(err)
continue
}
for _, c := range cs {
go func(c models.Campaign) {
processCampaign(&c)
}(c)
}
} }
} }