Skip to content

Commit

Permalink
Refactored result updating to be in result.go.
Browse files Browse the repository at this point in the history
Added the modified_date field to results so it's easy to keep track of the last results that were modified without having to parse every event. Updated the tests to reflect the changes.
  • Loading branch information
jordan-wright committed May 27, 2018
1 parent 222399c commit 420410b
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 147 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ gophish_admin.crt
gophish_admin.key

*.exe
*.db
gophish.db*
gophish
43 changes: 9 additions & 34 deletions controllers/phish.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package controllers

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
Expand All @@ -26,13 +25,6 @@ var ErrInvalidRequest = errors.New("Invalid request")
// has already been marked as complete.
var ErrCampaignComplete = errors.New("Event received on completed campaign")

// eventDetails is a struct that wraps common attributes we want to store
// in an event
type eventDetails struct {
Payload url.Values `json:"payload"`
Browser map[string]string `json:"browser"`
}

// CreatePhishingRouter creates the router that handles phishing connections.
func CreatePhishingRouter() http.Handler {
router := mux.NewRouter()
Expand All @@ -59,16 +51,8 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_OPENED, Details: string(rj)})
// Don't update the status if the user already clicked the link
// or submitted data to the campaign
if rs.Status == models.EVENT_CLICKED || rs.Status == models.EVENT_DATA_SUBMIT {
http.ServeFile(w, r, "static/images/pixel.png")
return
}
err = rs.UpdateStatus(models.EVENT_OPENED)
d := ctx.Get(r, "details").(models.EventDetails)
err = rs.HandleEmailOpened(d)
if err != nil {
log.Error(err)
}
Expand All @@ -87,11 +71,9 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_REPORTED, Details: string(rj)})
d := ctx.Get(r, "details").(models.EventDetails)

err = rs.UpdateReported(true)
err = rs.HandleEmailReport(d)
if err != nil {
log.Error(err)
}
Expand All @@ -112,7 +94,7 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
d := ctx.Get(r, "details").(models.EventDetails)
p, err := models.GetPage(c.PageId, c.UserId)
if err != nil {
log.Error(err)
Expand All @@ -121,18 +103,12 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
}
switch {
case r.Method == "GET":
if rs.Status != models.EVENT_CLICKED && rs.Status != models.EVENT_DATA_SUBMIT {
rs.UpdateStatus(models.EVENT_CLICKED)
}
err = c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_CLICKED, Details: string(rj)})
err = rs.HandleClickedLink(d)
if err != nil {
log.Error(err)
}
case r.Method == "POST":
// If data was POST'ed, let's record it
rs.UpdateStatus(models.EVENT_DATA_SUBMIT)
// Store the data in an event
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_DATA_SUBMIT, Details: string(rj)})
err = rs.HandleFormSubmit(d)
if err != nil {
log.Error(err)
}
Expand Down Expand Up @@ -224,16 +200,15 @@ func setupContext(r *http.Request) (error, *http.Request) {
if err != nil {
log.Error(err)
}
d := eventDetails{
d := models.EventDetails{
Payload: r.Form,
Browser: make(map[string]string),
}
d.Browser["address"] = ip
d.Browser["user-agent"] = r.Header.Get("User-Agent")
rj, err := json.Marshal(d)

r = ctx.Set(r, "result", rs)
r = ctx.Set(r, "campaign", c)
r = ctx.Set(r, "details", rj)
r = ctx.Set(r, "details", d)
return nil, r
}
10 changes: 9 additions & 1 deletion controllers/phish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {

campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_OPENED)
s.Equal(lastEvent.Message, models.EVENT_OPENED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}

func (s *ControllersSuite) TestReportedPhishingEmail() {
Expand All @@ -78,8 +81,10 @@ func (s *ControllersSuite) TestReportedPhishingEmail() {

campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Reported, true)
s.Equal(campaign.Events[len(campaign.Events)-1].Message, models.EVENT_REPORTED)
s.Equal(lastEvent.Message, models.EVENT_REPORTED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}

func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
Expand All @@ -92,7 +97,10 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {

campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_CLICKED)
s.Equal(lastEvent.Message, models.EVENT_CLICKED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}

func (s *ControllersSuite) TestNoRecipientID() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN modified_date DATETIME;

UPDATE results
SET `modified_date`= (
SELECT max(events.time) FROM events
WHERE events.email=results.email
AND events.campaign_id=results.campaign_id
);



-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN modified_date DATETIME;

UPDATE results
SET `modified_date`= (
SELECT max(events.time) FROM events
WHERE events.email=results.email
AND events.campaign_id=results.campaign_id
);



-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

61 changes: 38 additions & 23 deletions models/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package models

import (
"errors"
"net/url"
"time"

log "github.com/gophish/gophish/logger"
Expand Down Expand Up @@ -68,6 +69,30 @@ type CampaignStats struct {
Error int64 `json:"error"`
}

// Event contains the fields for an event
// that occurs during the campaign
type Event struct {
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
Email string `json:"email"`
Time time.Time `json:"time"`
Message string `json:"message"`
Details string `json:"details"`
}

// EventDetails is a struct that wraps common attributes we want to store
// in an event
type EventDetails struct {
Payload url.Values `json:"payload"`
Browser map[string]string `json:"browser"`
}

// EventError is a struct that wraps an error that occurs when sending an
// email to a recipient
type EventError struct {
Error string `json:"error"`
}

// ErrCampaignNameNotSpecified indicates there was no template given by the user
var ErrCampaignNameNotSpecified = errors.New("Campaign name not specified")

Expand Down Expand Up @@ -122,10 +147,10 @@ func (c *Campaign) UpdateStatus(s string) error {
}

// AddEvent creates a new campaign event in the database
func (c *Campaign) AddEvent(e Event) error {
func (c *Campaign) AddEvent(e *Event) error {
e.CampaignId = c.Id
e.Time = time.Now().UTC()
return db.Save(&e).Error
return db.Save(e).Error
}

// getDetails retrieves the related attributes of the campaign
Expand Down Expand Up @@ -220,17 +245,6 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
return s, err
}

// Event contains the fields for an event
// that occurs during the campaign
type Event struct {
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
Email string `json:"email"`
Time time.Time `json:"time"`
Message string `json:"message"`
Details string `json:"details"`
}

// GetCampaigns returns the campaigns owned by the given user.
func GetCampaigns(uid int64) ([]Campaign, error) {
cs := []Campaign{}
Expand Down Expand Up @@ -422,7 +436,7 @@ func PostCampaign(c *Campaign, uid int64) error {
log.Error(err)
return err
}
err = c.AddEvent(Event{Message: "Campaign Created"})
err = c.AddEvent(&Event{Message: "Campaign Created"})
if err != nil {
log.Error(err)
}
Expand All @@ -438,15 +452,16 @@ func PostCampaign(c *Campaign, uid int64) error {
}
resultMap[t.Email] = true
r := &Result{
Email: t.Email,
Position: t.Position,
Status: STATUS_SCHEDULED,
CampaignId: c.Id,
UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
Email: t.Email,
Position: t.Position,
Status: STATUS_SCHEDULED,
CampaignId: c.Id,
UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
ModifiedDate: c.CreatedDate,
}
if c.Status == CAMPAIGN_IN_PROGRESS {
r.Status = STATUS_SENDING
Expand Down
62 changes: 7 additions & 55 deletions models/maillog.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package models
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -58,30 +57,24 @@ func GenerateMailLog(c *Campaign, r *Result) error {
// too many times. Backoff also unlocks the maillog so that it can be processed
// again in the future.
func (m *MailLog) Backoff(reason error) error {
if m.SendAttempt == MaxSendAttempts {
err = m.addError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
r, err := GetResult(m.RId)
if err != nil {
return err
}
if m.SendAttempt == MaxSendAttempts {
r.HandleEmailError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
// Add an error, since we had to backoff because of a
// temporary error of some sort during the SMTP transaction
err = m.addError(reason)
if err != nil {
return err
}
m.SendAttempt++
backoffDuration := math.Pow(2, float64(m.SendAttempt))
m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
err = db.Save(m).Error
if err != nil {
return err
}
r.Status = STATUS_RETRY
r.SendDate = m.SendDate
err = db.Save(r).Error
err = r.HandleEmailBackoff(reason, m.SendDate)
if err != nil {
return err
}
Expand All @@ -101,32 +94,6 @@ func (m *MailLog) Lock() error {
return db.Save(&m).Error
}

// addError adds an error to the associated campaign
func (m *MailLog) addError(e error) error {
c, err := GetCampaign(m.CampaignId, m.UserId)
if err != nil {
return err
}
// This is redundant in the case of permanent
// errors, but the extra query makes for
// a cleaner API.
r, err := GetResult(m.RId)
if err != nil {
return err
}
es := struct {
Error string `json:"error"`
}{
Error: e.Error(),
}
ej, err := json.Marshal(es)
if err != nil {
log.Warn(err)
}
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENDING_ERROR, Details: string(ej)})
return err
}

// Error sets the error status on the models.Result that the
// maillog refers to. Since MailLog errors are permanent,
// this action also deletes the maillog.
Expand All @@ -136,14 +103,7 @@ func (m *MailLog) Error(e error) error {
log.Warn(err)
return err
}
// Update the result
err = r.UpdateStatus(ERROR)
if err != nil {
log.Warn(err)
return err
}
// Update the campaign events
err = m.addError(e)
err = r.HandleEmailError(e)
if err != nil {
log.Warn(err)
return err
Expand All @@ -159,15 +119,7 @@ func (m *MailLog) Success() error {
if err != nil {
return err
}
err = r.UpdateStatus(EVENT_SENT)
if err != nil {
return err
}
c, err := GetCampaign(m.CampaignId, m.UserId)
if err != nil {
return err
}
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENT})
err = r.HandleEmailSent()
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 420410b

Please sign in to comment.