mirror of
https://github.com/gophish/gophish
synced 2024-11-14 16:27:23 +00:00
Refactoring API into separate package for easier management. (#1411)
This commit is contained in:
parent
8b27d852d8
commit
1e0a78db30
16 changed files with 1105 additions and 851 deletions
|
@ -1,772 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jordan-wright/email"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// APIReset (/api/reset) resets the currently authenticated user's API key
|
||||
func (as *AdminServer) APIReset(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = auth.GenerateSecureKey()
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: true, Message: "API Key successfully reset!", Data: u.ApiKey}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaigns returns a list of campaigns if requested via GET.
|
||||
// If requested via POST, APICampaigns creates a new campaign and returns a reference to it.
|
||||
func (as *AdminServer) APICampaigns(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaigns(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
//POST: Create a new campaign and return it as JSON
|
||||
case r.Method == "POST":
|
||||
c := models.Campaign{}
|
||||
// Put the request into a campaign
|
||||
err := json.NewDecoder(r.Body).Decode(&c)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = models.PostCampaign(&c, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// If the campaign is scheduled to launch immediately, send it to the worker.
|
||||
// Otherwise, the worker will pick it up at the scheduled time
|
||||
if c.Status == models.CampaignInProgress {
|
||||
go as.worker.LaunchCampaign(c)
|
||||
}
|
||||
JSONResponse(w, c, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignsSummary returns the summary for the current user's campaigns
|
||||
func (as *AdminServer) APICampaignsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaign returns details about the requested campaign. If the campaign is not
|
||||
// valid, APICampaign returns null.
|
||||
func (as *AdminServer) APICampaign(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, c, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteCampaign(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign deleted successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignResults returns just the results for a given campaign to
|
||||
// significantly reduce the information returned.
|
||||
func (as *AdminServer) APICampaignResults(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
cr, err := models.GetCampaignResults(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method == "GET" {
|
||||
JSONResponse(w, cr, http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignSummary returns the summary for a given campaign.
|
||||
func (as *AdminServer) APICampaignSummary(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
}
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignComplete effectively "ends" a campaign.
|
||||
// Future phishing emails clicked will return a simple "404" page.
|
||||
func (as *AdminServer) APICampaignComplete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
err := models.CompleteCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error completing campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign completed successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroups returns a list of groups if requested via GET.
|
||||
// If requested via POST, APIGroups creates a new group and returns a reference to it.
|
||||
func (as *AdminServer) APIGroups(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroups(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "No groups found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
//POST: Create a new group and return it as JSON
|
||||
case r.Method == "POST":
|
||||
g := models.Group{}
|
||||
// Put the request into a group
|
||||
err := json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroupsSummary returns a summary of the groups owned by the current user.
|
||||
func (as *AdminServer) APIGroupsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroup returns details about the requested group.
|
||||
// If the group is not valid, APIGroup returns null.
|
||||
func (as *AdminServer) APIGroup(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting group"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Group deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroupSummary returns a summary of the groups owned by the current user.
|
||||
func (as *AdminServer) APIGroupSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APITemplates handles the functionality for the /api/templates endpoint
|
||||
func (as *AdminServer) APITemplates(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ts, err := models.GetTemplates(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
//POST: Create a new template and return it as JSON
|
||||
case r.Method == "POST":
|
||||
t := models.Template{}
|
||||
// Put the request into a template
|
||||
err := json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetTemplateByName(t.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostTemplate(&t)
|
||||
if err == models.ErrTemplateNameNotSpecified {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err == models.ErrTemplateMissingParameter {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error inserting template into database"}, http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APITemplate handles the functions for the /api/templates/:id endpoint
|
||||
func (as *AdminServer) APITemplate(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
t, err := models.GetTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting template"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Template deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
t = models.Template{}
|
||||
err = json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if t.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and template_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutTemplate(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIPages handles requests for the /api/pages/ endpoint
|
||||
func (as *AdminServer) APIPages(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ps, err := models.GetPages(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ps, http.StatusOK)
|
||||
//POST: Create a new page and return it as JSON
|
||||
case r.Method == "POST":
|
||||
p := models.Page{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetPageByName(p.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APIPage contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a Page object
|
||||
func (as *AdminServer) APIPage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
p, err := models.GetPage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeletePage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Page Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
p = models.Page{}
|
||||
err = json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if p.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page: " + err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APISendingProfiles handles requests for the /api/smtp/ endpoint
|
||||
func (as *AdminServer) APISendingProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ss, err := models.GetSMTPs(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ss, http.StatusOK)
|
||||
//POST: Create a new SMTP and return it as JSON
|
||||
case r.Method == "POST":
|
||||
s := models.SMTP{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetSMTPByName(s.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APISendingProfile contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a SMTP object
|
||||
func (as *AdminServer) APISendingProfile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
s, err := models.GetSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting SMTP"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "SMTP Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
s = models.SMTP{}
|
||||
err = json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if s.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:smtp_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIImportGroup imports a CSV of group members
|
||||
func (as *AdminServer) APIImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||
ts, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APIImportEmail allows for the importing of email.
|
||||
// Returns a Message object
|
||||
func (as *AdminServer) APIImportEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ir := struct {
|
||||
Content string `json:"content"`
|
||||
ConvertLinks bool `json:"convert_links"`
|
||||
}{}
|
||||
err := json.NewDecoder(r.Body).Decode(&ir)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e, err := email.NewEmailFromReader(strings.NewReader(ir.Content))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// If the user wants to convert links to point to
|
||||
// the landing page, let's make it happen by changing up
|
||||
// e.HTML
|
||||
if ir.ConvertLinks {
|
||||
d, err := goquery.NewDocumentFromReader(bytes.NewReader(e.HTML))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d.Find("a").Each(func(i int, a *goquery.Selection) {
|
||||
a.SetAttr("href", "{{.URL}}")
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
e.HTML = []byte(h)
|
||||
}
|
||||
er := emailResponse{
|
||||
Subject: e.Subject,
|
||||
Text: string(e.Text),
|
||||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APIImportSite allows for the importing of HTML from a website
|
||||
// Without "include_resources" set, it will merely place a "base" tag
|
||||
// so that all resources can be loaded relative to the given URL.
|
||||
func (as *AdminServer) APIImportSite(w http.ResponseWriter, r *http.Request) {
|
||||
cr := cloneRequest{}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cr)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err = cr.validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Get(cr.URL)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Insert the base href tag to better handle relative resources
|
||||
d, err := goquery.NewDocumentFromResponse(resp)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Assuming we don't want to include resources, we'll need a base href
|
||||
if d.Find("head base").Length() == 0 {
|
||||
d.Find("head").PrependHtml(fmt.Sprintf("<base href=\"%s\">", cr.URL))
|
||||
}
|
||||
forms := d.Find("form")
|
||||
forms.Each(func(i int, f *goquery.Selection) {
|
||||
// We'll want to store where we got the form from
|
||||
// (the current URL)
|
||||
url := f.AttrOr("action", cr.URL)
|
||||
if !strings.HasPrefix(url, "http") {
|
||||
url = fmt.Sprintf("%s%s", cr.URL, url)
|
||||
}
|
||||
f.PrependHtml(fmt.Sprintf("<input type=\"hidden\" name=\"__original_url\" value=\"%s\"/>", url))
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APISendTestEmail sends a test email using the template name
|
||||
// and Target given.
|
||||
func (as *AdminServer) APISendTestEmail(w http.ResponseWriter, r *http.Request) {
|
||||
s := &models.EmailRequest{
|
||||
ErrorChan: make(chan error),
|
||||
UserId: ctx.Get(r, "user_id").(int64),
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
storeRequest := false
|
||||
|
||||
// If a Template is not specified use a default
|
||||
if s.Template.Name == "" {
|
||||
//default message body
|
||||
text := "It works!\n\nThis is an email letting you know that your gophish\nconfiguration was successful.\n" +
|
||||
"Here are the details:\n\nWho you sent from: {{.From}}\n\nWho you sent to: \n" +
|
||||
"{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" +
|
||||
"{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" +
|
||||
"{{if .Position}} Position: {{.Position}}\n{{end}}" +
|
||||
"\nNow go send some phish!"
|
||||
t := models.Template{
|
||||
Subject: "Default Email from Gophish",
|
||||
Text: text,
|
||||
}
|
||||
s.Template = t
|
||||
} else {
|
||||
// Get the Template requested by name
|
||||
s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"template": s.Template.Name,
|
||||
}).Error("Template does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrTemplateNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.TemplateId = s.Template.Id
|
||||
// We'll only save the test request to the database if there is a
|
||||
// user-specified template to use.
|
||||
storeRequest = true
|
||||
}
|
||||
|
||||
if s.Page.Name != "" {
|
||||
s.Page, err = models.GetPageByName(s.Page.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"page": s.Page.Name,
|
||||
}).Error("Page does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.PageId = s.Page.Id
|
||||
}
|
||||
|
||||
// If a complete sending profile is provided use it
|
||||
if err := s.SMTP.Validate(); err != nil {
|
||||
// Otherwise get the SMTP requested by name
|
||||
smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId)
|
||||
// If the Sending Profile doesn't exist, let's err on the side
|
||||
// of caution and assume that the validation failure was more important.
|
||||
if lookupErr != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.SMTP = smtp
|
||||
}
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
|
||||
// Validate the given request
|
||||
if err = s.Validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the request if this wasn't the default template
|
||||
if storeRequest {
|
||||
err = models.PostEmailRequest(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Send the test email
|
||||
err = as.worker.SendTestEmail(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// JSONResponse attempts to set the status code, c, and marshal the given interface, d, into a response that
|
||||
// is written to the given ResponseWriter.
|
||||
func JSONResponse(w http.ResponseWriter, d interface{}, c int) {
|
||||
dj, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, "Error creating JSON response", http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(c)
|
||||
fmt.Fprintf(w, "%s", dj)
|
||||
}
|
||||
|
||||
type cloneRequest struct {
|
||||
URL string `json:"url"`
|
||||
IncludeResources bool `json:"include_resources"`
|
||||
}
|
||||
|
||||
func (cr *cloneRequest) validate() error {
|
||||
if cr.URL == "" {
|
||||
return errors.New("No URL Specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloneResponse struct {
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
122
controllers/api/api_test.go
Normal file
122
controllers/api/api_test.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type APISuite struct {
|
||||
suite.Suite
|
||||
apiKey string
|
||||
config *config.Config
|
||||
apiServer *Server
|
||||
}
|
||||
|
||||
func (s *APISuite) SetupSuite() {
|
||||
conf := &config.Config{
|
||||
DBName: "sqlite3",
|
||||
DBPath: ":memory:",
|
||||
MigrationsPath: "../../db/db_sqlite3/migrations/",
|
||||
}
|
||||
err := models.Setup(conf)
|
||||
if err != nil {
|
||||
s.T().Fatalf("Failed creating database: %v", err)
|
||||
}
|
||||
s.config = conf
|
||||
s.Nil(err)
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
// Move our cwd up to the project root for help with resolving
|
||||
// static assets
|
||||
err = os.Chdir("../")
|
||||
s.Nil(err)
|
||||
s.apiServer = NewServer()
|
||||
}
|
||||
|
||||
func (s *APISuite) TearDownTest() {
|
||||
campaigns, _ := models.GetCampaigns(1)
|
||||
for _, campaign := range campaigns {
|
||||
models.DeleteCampaign(campaign.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APISuite) SetupTest() {
|
||||
// Add a group
|
||||
group := models.Group{Name: "Test Group"}
|
||||
group.Targets = []models.Target{
|
||||
models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
|
||||
models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
|
||||
}
|
||||
group.UserId = 1
|
||||
models.PostGroup(&group)
|
||||
|
||||
// Add a template
|
||||
t := models.Template{Name: "Test Template"}
|
||||
t.Subject = "Test subject"
|
||||
t.Text = "Text text"
|
||||
t.HTML = "<html>Test</html>"
|
||||
t.UserId = 1
|
||||
models.PostTemplate(&t)
|
||||
|
||||
// Add a landing page
|
||||
p := models.Page{Name: "Test Page"}
|
||||
p.HTML = "<html>Test</html>"
|
||||
p.UserId = 1
|
||||
models.PostPage(&p)
|
||||
|
||||
// Add a sending profile
|
||||
smtp := models.SMTP{Name: "Test Page"}
|
||||
smtp.UserId = 1
|
||||
smtp.Host = "example.com"
|
||||
smtp.FromAddress = "test@test.com"
|
||||
models.PostSMTP(&smtp)
|
||||
|
||||
// Setup and "launch" our campaign
|
||||
// Set the status such that no emails are attempted
|
||||
c := models.Campaign{Name: "Test campaign"}
|
||||
c.UserId = 1
|
||||
c.Template = t
|
||||
c.Page = p
|
||||
c.SMTP = smtp
|
||||
c.Groups = []models.Group{group}
|
||||
models.PostCampaign(&c, c.UserId)
|
||||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestSiteImportBaseHref() {
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
hr := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
defer ts.Close()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/import/site",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
{
|
||||
"url" : "%s",
|
||||
"include_resources" : false
|
||||
}
|
||||
`, ts.URL))))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
s.apiServer.ImportSite(response, req)
|
||||
cs := cloneResponse{}
|
||||
err := json.NewDecoder(response.Body).Decode(&cs)
|
||||
s.Nil(err)
|
||||
s.Equal(cs.HTML, hr)
|
||||
}
|
||||
|
||||
func TestAPISuite(t *testing.T) {
|
||||
suite.Run(t, new(APISuite))
|
||||
}
|
137
controllers/api/campaign.go
Normal file
137
controllers/api/campaign.go
Normal file
|
@ -0,0 +1,137 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Campaigns returns a list of campaigns if requested via GET.
|
||||
// If requested via POST, APICampaigns creates a new campaign and returns a reference to it.
|
||||
func (as *Server) Campaigns(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaigns(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
//POST: Create a new campaign and return it as JSON
|
||||
case r.Method == "POST":
|
||||
c := models.Campaign{}
|
||||
// Put the request into a campaign
|
||||
err := json.NewDecoder(r.Body).Decode(&c)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = models.PostCampaign(&c, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// If the campaign is scheduled to launch immediately, send it to the worker.
|
||||
// Otherwise, the worker will pick it up at the scheduled time
|
||||
if c.Status == models.CampaignInProgress {
|
||||
go as.worker.LaunchCampaign(c)
|
||||
}
|
||||
JSONResponse(w, c, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// CampaignsSummary returns the summary for the current user's campaigns
|
||||
func (as *Server) CampaignsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// Campaign returns details about the requested campaign. If the campaign is not
|
||||
// valid, APICampaign returns null.
|
||||
func (as *Server) Campaign(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, c, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteCampaign(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign deleted successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// CampaignResults returns just the results for a given campaign to
|
||||
// significantly reduce the information returned.
|
||||
func (as *Server) CampaignResults(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
cr, err := models.GetCampaignResults(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method == "GET" {
|
||||
JSONResponse(w, cr, http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// CampaignSummary returns the summary for a given campaign.
|
||||
func (as *Server) CampaignSummary(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
}
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// CampaignComplete effectively "ends" a campaign.
|
||||
// Future phishing emails clicked will return a simple "404" page.
|
||||
func (as *Server) CampaignComplete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
err := models.CompleteCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error completing campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign completed successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
118
controllers/api/group.go
Normal file
118
controllers/api/group.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Groups returns a list of groups if requested via GET.
|
||||
// If requested via POST, APIGroups creates a new group and returns a reference to it.
|
||||
func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroups(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "No groups found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
//POST: Create a new group and return it as JSON
|
||||
case r.Method == "POST":
|
||||
g := models.Group{}
|
||||
// Put the request into a group
|
||||
err := json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// GroupsSummary returns a summary of the groups owned by the current user.
|
||||
func (as *Server) GroupsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// Group returns details about the requested group.
|
||||
// If the group is not valid, Group returns null.
|
||||
func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting group"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Group deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// GroupSummary returns a summary of the groups owned by the current user.
|
||||
func (as *Server) GroupSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
157
controllers/api/import.go
Normal file
157
controllers/api/import.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
type cloneRequest struct {
|
||||
URL string `json:"url"`
|
||||
IncludeResources bool `json:"include_resources"`
|
||||
}
|
||||
|
||||
func (cr *cloneRequest) validate() error {
|
||||
if cr.URL == "" {
|
||||
return errors.New("No URL Specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloneResponse struct {
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
// ImportGroup imports a CSV of group members
|
||||
func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||
ts, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportEmail allows for the importing of email.
|
||||
// Returns a Message object
|
||||
func (as *Server) ImportEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ir := struct {
|
||||
Content string `json:"content"`
|
||||
ConvertLinks bool `json:"convert_links"`
|
||||
}{}
|
||||
err := json.NewDecoder(r.Body).Decode(&ir)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e, err := email.NewEmailFromReader(strings.NewReader(ir.Content))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// If the user wants to convert links to point to
|
||||
// the landing page, let's make it happen by changing up
|
||||
// e.HTML
|
||||
if ir.ConvertLinks {
|
||||
d, err := goquery.NewDocumentFromReader(bytes.NewReader(e.HTML))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d.Find("a").Each(func(i int, a *goquery.Selection) {
|
||||
a.SetAttr("href", "{{.URL}}")
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
e.HTML = []byte(h)
|
||||
}
|
||||
er := emailResponse{
|
||||
Subject: e.Subject,
|
||||
Text: string(e.Text),
|
||||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportSite allows for the importing of HTML from a website
|
||||
// Without "include_resources" set, it will merely place a "base" tag
|
||||
// so that all resources can be loaded relative to the given URL.
|
||||
func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
|
||||
cr := cloneRequest{}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cr)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err = cr.validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Get(cr.URL)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Insert the base href tag to better handle relative resources
|
||||
d, err := goquery.NewDocumentFromResponse(resp)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Assuming we don't want to include resources, we'll need a base href
|
||||
if d.Find("head base").Length() == 0 {
|
||||
d.Find("head").PrependHtml(fmt.Sprintf("<base href=\"%s\">", cr.URL))
|
||||
}
|
||||
forms := d.Find("form")
|
||||
forms.Each(func(i int, f *goquery.Selection) {
|
||||
// We'll want to store where we got the form from
|
||||
// (the current URL)
|
||||
url := f.AttrOr("action", cr.URL)
|
||||
if !strings.HasPrefix(url, "http") {
|
||||
url = fmt.Sprintf("%s%s", cr.URL, url)
|
||||
}
|
||||
f.PrependHtml(fmt.Sprintf("<input type=\"hidden\" name=\"__original_url\" value=\"%s\"/>", url))
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
91
controllers/api/page.go
Normal file
91
controllers/api/page.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Pages handles requests for the /api/pages/ endpoint
|
||||
func (as *Server) Pages(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ps, err := models.GetPages(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ps, http.StatusOK)
|
||||
//POST: Create a new page and return it as JSON
|
||||
case r.Method == "POST":
|
||||
p := models.Page{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetPageByName(p.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// Page contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a Page object
|
||||
func (as *Server) Page(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
p, err := models.GetPage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeletePage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Page Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
p = models.Page{}
|
||||
err = json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if p.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page: " + err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
}
|
||||
}
|
24
controllers/api/reset.go
Normal file
24
controllers/api/reset.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
// Reset (/api/reset) resets the currently authenticated user's API key
|
||||
func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = auth.GenerateSecureKey()
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: true, Message: "API Key successfully reset!", Data: u.ApiKey}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
22
controllers/api/response.go
Normal file
22
controllers/api/response.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
// JSONResponse attempts to set the status code, c, and marshal the given interface, d, into a response that
|
||||
// is written to the given ResponseWriter.
|
||||
func JSONResponse(w http.ResponseWriter, d interface{}, c int) {
|
||||
dj, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, "Error creating JSON response", http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(c)
|
||||
fmt.Fprintf(w, "%s", dj)
|
||||
}
|
76
controllers/api/server.go
Normal file
76
controllers/api/server.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/worker"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ServerOption is an option to apply to the API server.
|
||||
type ServerOption func(*Server)
|
||||
|
||||
// Server represents the routes and functionality of the Gophish API.
|
||||
// It's not a server in the traditional sense, in that it isn't started and
|
||||
// stopped. Rather, it's meant to be used as an http.Handler in the
|
||||
// AdminServer.
|
||||
type Server struct {
|
||||
handler http.Handler
|
||||
worker worker.Worker
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the API handler with the provided
|
||||
// options applied.
|
||||
func NewServer(options ...ServerOption) *Server {
|
||||
defaultWorker, _ := worker.New()
|
||||
as := &Server{
|
||||
worker: defaultWorker,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(as)
|
||||
}
|
||||
as.registerRoutes()
|
||||
return as
|
||||
}
|
||||
|
||||
// WithWorker is an option that sets the background worker.
|
||||
func WithWorker(w worker.Worker) ServerOption {
|
||||
return func(as *Server) {
|
||||
as.worker = w
|
||||
}
|
||||
}
|
||||
|
||||
func (as *Server) registerRoutes() {
|
||||
root := mux.NewRouter()
|
||||
root = root.StrictSlash(true)
|
||||
router := root.PathPrefix("/api/").Subrouter()
|
||||
router.Use(mid.RequireAPIKey)
|
||||
router.Use(mid.EnforceViewOnly)
|
||||
router.HandleFunc("/reset", as.Reset)
|
||||
router.HandleFunc("/campaigns/", as.Campaigns)
|
||||
router.HandleFunc("/campaigns/summary", as.CampaignsSummary)
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", as.Campaign)
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}/results", as.CampaignResults)
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}/summary", as.CampaignSummary)
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}/complete", as.CampaignComplete)
|
||||
router.HandleFunc("/groups/", as.Groups)
|
||||
router.HandleFunc("/groups/summary", as.GroupsSummary)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}", as.Group)
|
||||
router.HandleFunc("/groups/{id:[0-9]+}/summary", as.GroupSummary)
|
||||
router.HandleFunc("/templates/", as.Templates)
|
||||
router.HandleFunc("/templates/{id:[0-9]+}", as.Template)
|
||||
router.HandleFunc("/pages/", as.Pages)
|
||||
router.HandleFunc("/pages/{id:[0-9]+}", as.Page)
|
||||
router.HandleFunc("/smtp/", as.SendingProfiles)
|
||||
router.HandleFunc("/smtp/{id:[0-9]+}", as.SendingProfile)
|
||||
router.HandleFunc("/util/send_test_email", as.SendTestEmail)
|
||||
router.HandleFunc("/import/group", as.ImportGroup)
|
||||
router.HandleFunc("/import/email", as.ImportEmail)
|
||||
router.HandleFunc("/import/site", as.ImportSite)
|
||||
as.handler = router
|
||||
}
|
||||
|
||||
func (as *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
as.handler.ServeHTTP(w, r)
|
||||
}
|
96
controllers/api/smtp.go
Normal file
96
controllers/api/smtp.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// SendingProfiles handles requests for the /api/smtp/ endpoint
|
||||
func (as *Server) SendingProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ss, err := models.GetSMTPs(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ss, http.StatusOK)
|
||||
//POST: Create a new SMTP and return it as JSON
|
||||
case r.Method == "POST":
|
||||
s := models.SMTP{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetSMTPByName(s.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// SendingProfile contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a SMTP object
|
||||
func (as *Server) SendingProfile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
s, err := models.GetSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting SMTP"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "SMTP Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
s = models.SMTP{}
|
||||
err = json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if s.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:smtp_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
}
|
||||
}
|
97
controllers/api/template.go
Normal file
97
controllers/api/template.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Templates handles the functionality for the /api/templates endpoint
|
||||
func (as *Server) Templates(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ts, err := models.GetTemplates(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
//POST: Create a new template and return it as JSON
|
||||
case r.Method == "POST":
|
||||
t := models.Template{}
|
||||
// Put the request into a template
|
||||
err := json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetTemplateByName(t.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostTemplate(&t)
|
||||
if err == models.ErrTemplateNameNotSpecified {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err == models.ErrTemplateMissingParameter {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error inserting template into database"}, http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// Template handles the functions for the /api/templates/:id endpoint
|
||||
func (as *Server) Template(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
t, err := models.GetTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting template"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Template deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
t = models.Template{}
|
||||
err = json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if t.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and template_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutTemplate(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
}
|
||||
}
|
122
controllers/api/util.go
Normal file
122
controllers/api/util.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SendTestEmail sends a test email using the template name
|
||||
// and Target given.
|
||||
func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
|
||||
s := &models.EmailRequest{
|
||||
ErrorChan: make(chan error),
|
||||
UserId: ctx.Get(r, "user_id").(int64),
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
storeRequest := false
|
||||
|
||||
// If a Template is not specified use a default
|
||||
if s.Template.Name == "" {
|
||||
//default message body
|
||||
text := "It works!\n\nThis is an email letting you know that your gophish\nconfiguration was successful.\n" +
|
||||
"Here are the details:\n\nWho you sent from: {{.From}}\n\nWho you sent to: \n" +
|
||||
"{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" +
|
||||
"{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" +
|
||||
"{{if .Position}} Position: {{.Position}}\n{{end}}" +
|
||||
"\nNow go send some phish!"
|
||||
t := models.Template{
|
||||
Subject: "Default Email from Gophish",
|
||||
Text: text,
|
||||
}
|
||||
s.Template = t
|
||||
} else {
|
||||
// Get the Template requested by name
|
||||
s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"template": s.Template.Name,
|
||||
}).Error("Template does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrTemplateNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.TemplateId = s.Template.Id
|
||||
// We'll only save the test request to the database if there is a
|
||||
// user-specified template to use.
|
||||
storeRequest = true
|
||||
}
|
||||
|
||||
if s.Page.Name != "" {
|
||||
s.Page, err = models.GetPageByName(s.Page.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"page": s.Page.Name,
|
||||
}).Error("Page does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.PageId = s.Page.Id
|
||||
}
|
||||
|
||||
// If a complete sending profile is provided use it
|
||||
if err := s.SMTP.Validate(); err != nil {
|
||||
// Otherwise get the SMTP requested by name
|
||||
smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId)
|
||||
// If the Sending Profile doesn't exist, let's err on the side
|
||||
// of caution and assume that the validation failure was more important.
|
||||
if lookupErr != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.SMTP = smtp
|
||||
}
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
|
||||
// Validate the given request
|
||||
if err = s.Validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the request if this wasn't the default template
|
||||
if storeRequest {
|
||||
err = models.PostEmailRequest(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Send the test email
|
||||
err = as.worker.SendTestEmail(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -103,52 +99,6 @@ func (s *ControllersSuite) SetupTest() {
|
|||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestRequireAPIKey() {
|
||||
resp, err := http.Post(fmt.Sprintf("%s/api/import/site", s.adminServer.URL), "application/json", nil)
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidAPIKey() {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/api/groups/?api_key=%s", s.adminServer.URL, "bogus-api-key"))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestBearerToken() {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/groups/", s.adminServer.URL), nil)
|
||||
s.Nil(err)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestSiteImportBaseHref() {
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
hr := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
defer ts.Close()
|
||||
resp, err := http.Post(fmt.Sprintf("%s/api/import/site?api_key=%s", s.adminServer.URL, s.apiKey), "application/json",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
{
|
||||
"url" : "%s",
|
||||
"include_resources" : false
|
||||
}
|
||||
`, ts.URL))))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
cs := cloneResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&cs)
|
||||
s.Nil(err)
|
||||
s.Equal(cs.HTML, hr)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TearDownSuite() {
|
||||
// Tear down the admin and phishing servers
|
||||
s.adminServer.Close()
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/gophish/gophish/config"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
|
@ -299,7 +300,7 @@ func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Req
|
|||
SendDate: rs.SendDate,
|
||||
ContactAddress: ps.contactAddress,
|
||||
}
|
||||
JSONResponse(w, tr, http.StatusOK)
|
||||
api.JSONResponse(w, tr, http.StatusOK)
|
||||
}
|
||||
|
||||
// setupContext handles some of the administrative work around receiving a new
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
|
@ -106,31 +107,8 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
// Create the API routes
|
||||
api := router.PathPrefix("/api").Subrouter()
|
||||
api = api.StrictSlash(true)
|
||||
api.Use(mid.RequireAPIKey)
|
||||
api.Use(mid.EnforceViewOnly)
|
||||
api.HandleFunc("/reset", as.APIReset)
|
||||
api.HandleFunc("/campaigns/", as.APICampaigns)
|
||||
api.HandleFunc("/campaigns/summary", as.APICampaignsSummary)
|
||||
api.HandleFunc("/campaigns/{id:[0-9]+}", as.APICampaign)
|
||||
api.HandleFunc("/campaigns/{id:[0-9]+}/results", as.APICampaignResults)
|
||||
api.HandleFunc("/campaigns/{id:[0-9]+}/summary", as.APICampaignSummary)
|
||||
api.HandleFunc("/campaigns/{id:[0-9]+}/complete", as.APICampaignComplete)
|
||||
api.HandleFunc("/groups/", as.APIGroups)
|
||||
api.HandleFunc("/groups/summary", as.APIGroupsSummary)
|
||||
api.HandleFunc("/groups/{id:[0-9]+}", as.APIGroup)
|
||||
api.HandleFunc("/groups/{id:[0-9]+}/summary", as.APIGroupSummary)
|
||||
api.HandleFunc("/templates/", as.APITemplates)
|
||||
api.HandleFunc("/templates/{id:[0-9]+}", as.APITemplate)
|
||||
api.HandleFunc("/pages/", as.APIPages)
|
||||
api.HandleFunc("/pages/{id:[0-9]+}", as.APIPage)
|
||||
api.HandleFunc("/smtp/", as.APISendingProfiles)
|
||||
api.HandleFunc("/smtp/{id:[0-9]+}", as.APISendingProfile)
|
||||
api.HandleFunc("/util/send_test_email", as.APISendTestEmail)
|
||||
api.HandleFunc("/import/group", as.APIImportGroup)
|
||||
api.HandleFunc("/import/email", as.APIImportEmail)
|
||||
api.HandleFunc("/import/site", as.APIImportSite)
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
router.PathPrefix("/api/").Handler(api)
|
||||
|
||||
// Setup static file serving
|
||||
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
|
||||
|
@ -280,16 +258,16 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
|
|||
if err == auth.ErrInvalidPassword {
|
||||
msg.Message = "Invalid Password"
|
||||
msg.Success = false
|
||||
JSONResponse(w, msg, http.StatusBadRequest)
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
JSONResponse(w, msg, http.StatusBadRequest)
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, msg, http.StatusOK)
|
||||
api.JSONResponse(w, msg, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
@ -17,6 +18,7 @@ var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reques
|
|||
|
||||
type MiddlewareSuite struct {
|
||||
suite.Suite
|
||||
apiKey string
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) SetupSuite() {
|
||||
|
@ -29,6 +31,10 @@ func (s *MiddlewareSuite) SetupSuite() {
|
|||
if err != nil {
|
||||
s.T().Fatalf("Failed creating database: %v", err)
|
||||
}
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
s.Nil(err)
|
||||
s.apiKey = u.ApiKey
|
||||
}
|
||||
|
||||
// MiddlewarePermissionTest maps an expected HTTP Method to an expected HTTP
|
||||
|
@ -99,6 +105,35 @@ func (s *MiddlewareSuite) TestRequirePermission() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestRequireAPIKey() {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
// Test that making a request without an API key is denied
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestInvalidAPIKey() {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
query := req.URL.Query()
|
||||
query.Set("api_key", "bogus-api-key")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *MiddlewareSuite) TestBearerToken() {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
RequireAPIKey(successHandler).ServeHTTP(response, req)
|
||||
s.Equal(response.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestMiddlewareSuite(t *testing.T) {
|
||||
suite.Run(t, new(MiddlewareSuite))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue