mirror of https://github.com/gophish/gophish
457 lines
14 KiB
Go
457 lines
14 KiB
Go
package controllers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
ctx "github.com/gorilla/context"
|
|
"github.com/gorilla/mux"
|
|
"github.com/jinzhu/gorm"
|
|
"github.com/jordan-wright/email"
|
|
"github.com/gophish/gophish/auth"
|
|
"github.com/gophish/gophish/models"
|
|
"github.com/gophish/gophish/util"
|
|
"github.com/gophish/gophish/worker"
|
|
)
|
|
|
|
// Worker is the worker that processes phishing events and updates campaigns.
|
|
var Worker *worker.Worker
|
|
|
|
func init() {
|
|
Worker = worker.New()
|
|
go Worker.Start()
|
|
}
|
|
|
|
// API (/api) provides access to api documentation
|
|
func API(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case r.Method == "GET":
|
|
templates := template.New("template")
|
|
_, err := templates.ParseFiles("templates/docs.html")
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
template.Must(templates, err).ExecuteTemplate(w, "base", nil)
|
|
}
|
|
}
|
|
|
|
// API (/api/reset) resets a user's API key
|
|
func API_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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// API_Campaigns returns a list of campaigns if requested via GET.
|
|
// If requested via POST, API_Campaigns creates a new campaign and returns a reference to it.
|
|
func API_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 {
|
|
fmt.Println(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
|
|
}
|
|
Worker.Queue <- &c
|
|
JSONResponse(w, c, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
// API_Campaigns_Id returns details about the requested campaign. If the campaign is not
|
|
// valid, API_Campaigns_Id returns null.
|
|
func API_Campaigns_Id(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 {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// API_Groups returns details about the requested group. If the campaign is not
|
|
// valid, API_Groups returns null.
|
|
func API_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.RecordNotFound {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
|
return
|
|
}
|
|
g.ModifiedDate = time.Now()
|
|
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
|
|
}
|
|
w.Header().Set("Location", "http://localhost:3333/api/groups/"+string(g.Id))
|
|
JSONResponse(w, g, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
// API_Groups_Id returns details about the requested campaign. If the campaign is not
|
|
// valid, API_Campaigns_Id returns null.
|
|
func API_Groups_Id(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()
|
|
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)
|
|
}
|
|
}
|
|
|
|
func API_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 {
|
|
fmt.Println(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.RecordNotFound {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Template name already in use"}, http.StatusConflict)
|
|
return
|
|
}
|
|
t.ModifiedDate = time.Now()
|
|
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)
|
|
Logger.Println(err)
|
|
return
|
|
}
|
|
JSONResponse(w, t, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
func API_Templates_Id(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 {
|
|
Logger.Println(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()
|
|
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)
|
|
}
|
|
}
|
|
|
|
// API_Pages handles requests for the /api/pages/ endpoint
|
|
func API_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 {
|
|
fmt.Println(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.RecordNotFound {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Page name already in use"}, http.StatusConflict)
|
|
Logger.Println(err)
|
|
return
|
|
}
|
|
p.ModifiedDate = time.Now()
|
|
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)
|
|
}
|
|
}
|
|
|
|
func API_Pages_Id(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 {
|
|
Logger.Println(err)
|
|
}
|
|
if p.Id != id {
|
|
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
err = p.Validate()
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Invalid attributes given"}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
p.ModifiedDate = time.Now()
|
|
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"}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
JSONResponse(w, p, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// API_Import_Group imports a CSV of group members
|
|
func API_Import_Group(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
|
|
}
|
|
|
|
// API_Import_Email allows for the importing of email.
|
|
// Returns a Message object
|
|
func API_Import_Email(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
e, err := email.NewEmailFromReader(r.Body)
|
|
if err != nil {
|
|
Logger.Println(err)
|
|
}
|
|
er := emailResponse{
|
|
Subject: e.Subject,
|
|
Text: string(e.Text),
|
|
HTML: string(e.HTML),
|
|
}
|
|
JSONResponse(w, er, http.StatusOK)
|
|
return
|
|
}
|
|
|
|
// API_Import_Site 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 API_Import_Site(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
|
|
}
|
|
resp, err := http.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.NewDocumentFromReader(resp.Body)
|
|
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))
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
Logger.Println(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"`
|
|
}
|