diff --git a/controllers/api.go b/controllers/api.go
deleted file mode 100644
index a4341355..00000000
--- a/controllers/api.go
+++ /dev/null
@@ -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("", 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("", 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"`
-}
diff --git a/controllers/api/api_test.go b/controllers/api/api_test.go
new file mode 100644
index 00000000..c4a4a8b7
--- /dev/null
+++ b/controllers/api/api_test.go
@@ -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 = "Test"
+ t.UserId = 1
+ models.PostTemplate(&t)
+
+ // Add a landing page
+ p := models.Page{Name: "Test Page"}
+ p.HTML = "Test"
+ 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 := "
"
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, h)
+ }))
+ hr := fmt.Sprintf("\n", 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))
+}
diff --git a/controllers/api/campaign.go b/controllers/api/campaign.go
new file mode 100644
index 00000000..33c08fe7
--- /dev/null
+++ b/controllers/api/campaign.go
@@ -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)
+ }
+}
diff --git a/controllers/api/group.go b/controllers/api/group.go
new file mode 100644
index 00000000..26dcfb39
--- /dev/null
+++ b/controllers/api/group.go
@@ -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)
+ }
+}
diff --git a/controllers/api/import.go b/controllers/api/import.go
new file mode 100644
index 00000000..f1b164d9
--- /dev/null
+++ b/controllers/api/import.go
@@ -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("", 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("", 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
+}
diff --git a/controllers/api/page.go b/controllers/api/page.go
new file mode 100644
index 00000000..bdebf5a7
--- /dev/null
+++ b/controllers/api/page.go
@@ -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)
+ }
+}
diff --git a/controllers/api/reset.go b/controllers/api/reset.go
new file mode 100644
index 00000000..1baf950d
--- /dev/null
+++ b/controllers/api/reset.go
@@ -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)
+ }
+ }
+}
diff --git a/controllers/api/response.go b/controllers/api/response.go
new file mode 100644
index 00000000..bf53014a
--- /dev/null
+++ b/controllers/api/response.go
@@ -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)
+}
diff --git a/controllers/api/server.go b/controllers/api/server.go
new file mode 100644
index 00000000..ba605424
--- /dev/null
+++ b/controllers/api/server.go
@@ -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)
+}
diff --git a/controllers/api/smtp.go b/controllers/api/smtp.go
new file mode 100644
index 00000000..2da33c93
--- /dev/null
+++ b/controllers/api/smtp.go
@@ -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)
+ }
+}
diff --git a/controllers/api/template.go b/controllers/api/template.go
new file mode 100644
index 00000000..da8cd90c
--- /dev/null
+++ b/controllers/api/template.go
@@ -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)
+ }
+}
diff --git a/controllers/api/util.go b/controllers/api/util.go
new file mode 100644
index 00000000..0f6c215c
--- /dev/null
+++ b/controllers/api/util.go
@@ -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
+}
diff --git a/controllers/api_test.go b/controllers/api_test.go
index 9f9e26d9..de0a7366 100644
--- a/controllers/api_test.go
+++ b/controllers/api_test.go
@@ -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 := ""
- ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, h)
- }))
- hr := fmt.Sprintf("\n", 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()
diff --git a/controllers/phish.go b/controllers/phish.go
index e2ff7870..a41708bc 100644
--- a/controllers/phish.go
+++ b/controllers/phish.go
@@ -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
diff --git a/controllers/route.go b/controllers/route.go
index 9adc3e4f..fd8beaa2 100644
--- a/controllers/route.go
+++ b/controllers/route.go
@@ -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)
}
}
diff --git a/middleware/middleware_test.go b/middleware/middleware_test.go
index b17b6351..30752b01 100644
--- a/middleware/middleware_test.go
+++ b/middleware/middleware_test.go
@@ -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))
}