2019-03-27 03:17:20 +00:00
package api
import (
"encoding/json"
"net/http"
"strconv"
2020-10-23 16:56:00 +00:00
"strings"
2019-03-27 03:17:20 +00:00
"time"
ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
2020-10-23 16:56:00 +00:00
"github.com/gophish/gophish/util"
2019-03-27 03:17:20 +00:00
"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 )
2020-10-23 16:56:00 +00:00
// POST: Create a new group and return it as JSON
2019-03-27 03:17:20 +00:00
case r . Method == "POST" :
g := models . Group { }
// Put the request into a group
2020-10-23 16:56:00 +00:00
// 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
}
2019-03-27 03:17:20 +00:00
}
2020-10-23 16:56:00 +00:00
_ , err := models . GetGroupByName ( g . Name , ctx . Get ( r , "user_id" ) . ( int64 ) )
2019-03-27 03:17:20 +00:00
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
}
2020-10-23 16:56:00 +00:00
// 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
}
2019-03-27 03:17:20 +00:00
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 )
2020-10-23 16:56:00 +00:00
// 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 )
2019-03-27 03:17:20 +00:00
if err != nil {
JSONResponse ( w , models . Response { Success : false , Message : "Group not found" } , http . StatusNotFound )
return
}
switch {
case r . Method == "GET" :
2020-10-23 16:56:00 +00:00
// 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 )
}
2019-03-27 03:17:20 +00:00
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 { }
2020-10-23 16:56:00 +00:00
//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
}
2020-05-26 02:46:36 +00:00
}
2019-03-27 03:17:20 +00:00
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
}
2020-10-23 16:56:00 +00:00
// 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
}
2019-03-27 03:17:20 +00:00
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 )
}
}
2020-10-23 16:56:00 +00:00
// 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 )
}
}