mirror of https://github.com/gophish/gophish
290 lines
11 KiB
Go
290 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
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"
|
|
)
|
|
|
|
// 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
|
|
|
|
// Check if content is CSV
|
|
// NB We can only upload one file at a time for new group creation, as three seperate POSTs are sent in quick succesion. We can't
|
|
// identify which group the 1+n files should be assigned to. Need to give this more thought, perhaps we can upload multiple files in one POST,
|
|
// or assign a temporary tracking token to link them together.
|
|
var csvmode = false
|
|
contentType := r.Header.Get("Content-Type")
|
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
|
csvmode = true
|
|
targets, groupname, err := util.ParseCSV(r)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
g.Name = groupname
|
|
g.Targets = targets
|
|
} else {
|
|
// else decode as JSON
|
|
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
|
|
}
|
|
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
|
// will still return the full list.
|
|
if csvmode == true {
|
|
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
|
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)
|
|
|
|
// Paramters passed by DataTables for pagination are handled below
|
|
v := r.URL.Query()
|
|
search := v.Get("search[value]")
|
|
|
|
sortcolumn := v.Get("order[0][column]")
|
|
sortdir := v.Get("order[0][dir]")
|
|
sortby := v.Get("columns[" + sortcolumn + "][data]")
|
|
order := sortby + " " + sortdir // e.g "first_name asc"
|
|
|
|
start, err := strconv.ParseInt(v.Get("start"), 0, 64)
|
|
if err != nil {
|
|
start = -1 // Default. gorm will ignore with this value.
|
|
}
|
|
length, err := strconv.ParseInt(v.Get("length"), 0, 64)
|
|
if err != nil {
|
|
length = -1 // Default. gorm will ignore with this value.
|
|
}
|
|
draw, err := strconv.ParseInt(v.Get("draw"), 0, 64)
|
|
if err != nil {
|
|
draw = -1 // If the draw value is missing we can assume this is not a DataTable request and return regular API result
|
|
}
|
|
|
|
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), start, length, search, order)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
|
return
|
|
}
|
|
switch {
|
|
case r.Method == "GET":
|
|
|
|
// If draw paratmer is -1 return regular API response, otherwise return pagination response
|
|
if draw == -1 {
|
|
JSONResponse(w, g, http.StatusOK)
|
|
} else {
|
|
// Handle pagination for DataTable
|
|
gs, _ := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64)) // We need to get the total number of records of the group
|
|
dT := models.DataTable{Draw: draw, RecordsTotal: gs.NumTargets, RecordsFiltered: gs.NumTargets}
|
|
dT.Data = make([]interface{}, len(g.Targets)) // Pseudocode of 'dT.Data = g.Targets'. https://golang.org/doc/faq#convert_slice_of_interface
|
|
for i, v := range g.Targets {
|
|
dT.Data[i] = v
|
|
}
|
|
JSONResponse(w, dT, 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{}
|
|
|
|
//Check if content is CSV
|
|
var csvmode = false
|
|
contentType := r.Header.Get("Content-Type")
|
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
|
csvmode = true
|
|
targets, groupname, err := util.ParseCSV(r)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
// We need to fetch all the existing targets for this group, so as to not overwrite them below
|
|
et, _ := models.GetTargets(id, -1, -1, "", "")
|
|
g.Targets = append(targets, et...)
|
|
g.Name = groupname
|
|
g.Id = id // ID isn't supplied in the CSV file upload. Perhaps we could use the filename paramter for this? I'm not sure if this is necessary though.
|
|
} else { // else JSON
|
|
err = json.NewDecoder(r.Body).Decode(&g)
|
|
if err != nil {
|
|
log.Errorf("error decoding group: %v", err)
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
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
|
|
}
|
|
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
|
|
// will still return the full list.
|
|
if csvmode == true {
|
|
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
|
|
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)
|
|
}
|
|
}
|
|
|
|
// GroupTarget handles interactions with individual targets
|
|
func (as *Server) GroupTarget(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
gid, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
|
|
|
// Ensure the group belongs to the user
|
|
_, err := models.GetGroup(gid, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
|
return
|
|
}
|
|
t := models.Target{}
|
|
err = json.NewDecoder(r.Body).Decode(&t)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Unable to decode target"}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
switch {
|
|
case r.Method == "PUT":
|
|
// Add an individual target to a group
|
|
err = models.AddTargetToGroup(t, gid)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Unable to add target to group"}, http.StatusNotFound)
|
|
return
|
|
}
|
|
JSONResponse(w, models.Response{Success: true, Message: "Added target to group"}, http.StatusCreated)
|
|
case r.Method == "DELETE":
|
|
|
|
err := models.DeleteTarget(&t, gid, ctx.Get(r, "user_id").(int64)) // We pass the group id to update modified date, and userid to ensure user owner the target
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
JSONResponse(w, models.Response{Success: true, Message: "Deleted target"}, http.StatusCreated)
|
|
}
|
|
}
|
|
|
|
// GroupRename handles renaming of a group (without supplying all the targets, as in Group() PUT above)
|
|
func (as *Server) GroupRename(w http.ResponseWriter, r *http.Request) {
|
|
|
|
vars := mux.Vars(r)
|
|
id, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
|
|
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case r.Method == "PUT":
|
|
g = models.Group{}
|
|
err = json.NewDecoder(r.Body).Decode(&g)
|
|
if err != nil {
|
|
log.Errorf("error decoding group: %v", err)
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if g.Id != id {
|
|
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
|
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.UpdateGroup(&g)
|
|
if err != nil {
|
|
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
|
return
|
|
}
|
|
JSONResponse(w, models.Response{Success: true, Message: "Group renamed"}, http.StatusCreated)
|
|
|
|
}
|
|
}
|