mirror of https://github.com/gophish/gophish
Merge branch 'master' into feature/spoofed-hostname
commit
905e02bdb8
|
@ -5,6 +5,8 @@ go:
|
|||
- 1.9
|
||||
- "1.10"
|
||||
- 1.11
|
||||
- 1.12
|
||||
- 1.13
|
||||
- tip
|
||||
|
||||
install:
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# Minify client side assets (JavaScript)
|
||||
FROM node:latest AS build-js
|
||||
|
||||
RUN npm install gulp gulp-cli -g
|
||||
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
RUN npm install --only=dev
|
||||
RUN gulp
|
||||
|
||||
|
||||
# Build Golang binary
|
||||
FROM golang:1.11 AS build-golang
|
||||
|
||||
WORKDIR /go/src/github.com/gophish/gophish
|
||||
COPY . .
|
||||
RUN go get -v && go build -v
|
||||
|
||||
|
||||
# Runtime container
|
||||
FROM debian:stable-slim
|
||||
|
||||
RUN useradd -m -d /opt/gophish -s /bin/bash app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y jq libcap2-bin && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
WORKDIR /opt/gophish
|
||||
COPY --from=build-golang /go/src/github.com/gophish/gophish/ ./
|
||||
COPY --from=build-js /build/static/js/dist/ ./static/js/dist/
|
||||
COPY --from=build-js /build/static/css/dist/ ./static/css/dist/
|
||||
COPY --from=build-golang /go/src/github.com/gophish/gophish/config.json ./
|
||||
RUN chown app. config.json
|
||||
|
||||
RUN setcap 'cap_net_bind_service=+ep' /opt/gophish/gophish
|
||||
|
||||
USER app
|
||||
RUN sed -i 's/127.0.0.1/0.0.0.0/g' config.json
|
||||
RUN touch config.json.tmp
|
||||
|
||||
EXPOSE 3333 8080 8443
|
||||
|
||||
CMD ["./docker/run.sh"]
|
83
auth/auth.go
83
auth/auth.go
|
@ -1,49 +1,24 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"crypto/rand"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jinzhu/gorm"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
//init registers the necessary models to be saved in the session later
|
||||
func init() {
|
||||
gob.Register(&models.User{})
|
||||
gob.Register(&models.Flash{})
|
||||
Store.Options.HttpOnly = true
|
||||
// This sets the maxAge to 5 days for all cookies
|
||||
Store.MaxAge(86400 * 5)
|
||||
}
|
||||
|
||||
// Store contains the session information for the request
|
||||
var Store = sessions.NewCookieStore(
|
||||
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
|
||||
[]byte(securecookie.GenerateRandomKey(32)))
|
||||
|
||||
// ErrInvalidPassword is thrown when a user provides an incorrect password.
|
||||
var ErrInvalidPassword = errors.New("Invalid Password")
|
||||
|
||||
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrPasswordMismatch = errors.New("Password cannot be blank")
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("Password cannot be blank")
|
||||
|
||||
// ErrPasswordMismatch is thrown when a user provides passwords that do not match
|
||||
var ErrPasswordMismatch = errors.New("Passwords must match")
|
||||
|
||||
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
||||
var ErrUsernameTaken = errors.New("Username already taken")
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// Login attempts to login the user given a request.
|
||||
func Login(r *http.Request) (bool, models.User, error) {
|
||||
|
@ -61,54 +36,6 @@ func Login(r *http.Request) (bool, models.User, error) {
|
|||
return true, u, nil
|
||||
}
|
||||
|
||||
// Register attempts to register the user given a request.
|
||||
func Register(r *http.Request) (bool, error) {
|
||||
username := r.FormValue("username")
|
||||
newPassword := r.FormValue("password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
// If the given username already exists, throw an error and return false
|
||||
if err == nil {
|
||||
return false, ErrUsernameTaken
|
||||
}
|
||||
|
||||
// If we have an error which is not simply indicating that no user was found, report it
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
log.Warn(err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
u = models.User{}
|
||||
// If we've made it here, we should have a valid username given
|
||||
// Check that the passsword isn't blank
|
||||
if newPassword == "" {
|
||||
return false, ErrEmptyPassword
|
||||
}
|
||||
// Make sure passwords match
|
||||
if newPassword != confirmPassword {
|
||||
return false, ErrPasswordMismatch
|
||||
}
|
||||
// Let's create the password hash
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
u.Username = username
|
||||
u.Hash = string(h)
|
||||
u.ApiKey = GenerateSecureKey()
|
||||
err = models.PutUser(&u)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GenerateSecureKey creates a secure key to use
|
||||
// as an API key
|
||||
func GenerateSecureKey() string {
|
||||
// Inspired from gorilla/securecookie
|
||||
k := make([]byte, 32)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// ChangePassword verifies the current password provided in the request and,
|
||||
// if it's valid, changes the password for the authenticated user.
|
||||
func ChangePassword(r *http.Request) error {
|
||||
|
|
|
@ -32,6 +32,7 @@ type Config struct {
|
|||
PhishConf PhishServer `json:"phish_server"`
|
||||
DBName string `json:"db_name"`
|
||||
DBPath string `json:"db_path"`
|
||||
DBSSLCaPath string `json:"db_sslca_path"`
|
||||
MigrationsPath string `json:"migrations_prefix"`
|
||||
TestFlag bool `json:"test_flag"`
|
||||
ContactAddress string `json:"contact_address"`
|
||||
|
|
|
@ -1,772 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jordan-wright/email"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// APIReset (/api/reset) resets the currently authenticated user's API key
|
||||
func (as *AdminServer) APIReset(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = auth.GenerateSecureKey()
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: true, Message: "API Key successfully reset!", Data: u.ApiKey}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaigns returns a list of campaigns if requested via GET.
|
||||
// If requested via POST, APICampaigns creates a new campaign and returns a reference to it.
|
||||
func (as *AdminServer) APICampaigns(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaigns(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
//POST: Create a new campaign and return it as JSON
|
||||
case r.Method == "POST":
|
||||
c := models.Campaign{}
|
||||
// Put the request into a campaign
|
||||
err := json.NewDecoder(r.Body).Decode(&c)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = models.PostCampaign(&c, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// If the campaign is scheduled to launch immediately, send it to the worker.
|
||||
// Otherwise, the worker will pick it up at the scheduled time
|
||||
if c.Status == models.CampaignInProgress {
|
||||
go as.worker.LaunchCampaign(c)
|
||||
}
|
||||
JSONResponse(w, c, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignsSummary returns the summary for the current user's campaigns
|
||||
func (as *AdminServer) APICampaignsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaign returns details about the requested campaign. If the campaign is not
|
||||
// valid, APICampaign returns null.
|
||||
func (as *AdminServer) APICampaign(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
c, err := models.GetCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, c, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteCampaign(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign deleted successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignResults returns just the results for a given campaign to
|
||||
// significantly reduce the information returned.
|
||||
func (as *AdminServer) APICampaignResults(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
cr, err := models.GetCampaignResults(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method == "GET" {
|
||||
JSONResponse(w, cr, http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignSummary returns the summary for a given campaign.
|
||||
func (as *AdminServer) APICampaignSummary(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
cs, err := models.GetCampaignSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Campaign not found"}, http.StatusNotFound)
|
||||
} else {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
}
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APICampaignComplete effectively "ends" a campaign.
|
||||
// Future phishing emails clicked will return a simple "404" page.
|
||||
func (as *AdminServer) APICampaignComplete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
err := models.CompleteCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error completing campaign"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Campaign completed successfully!"}, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroups returns a list of groups if requested via GET.
|
||||
// If requested via POST, APIGroups creates a new group and returns a reference to it.
|
||||
func (as *AdminServer) APIGroups(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroups(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "No groups found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
//POST: Create a new group and return it as JSON
|
||||
case r.Method == "POST":
|
||||
g := models.Group{}
|
||||
// Put the request into a group
|
||||
err := json.NewDecoder(r.Body).Decode(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroupsSummary returns a summary of the groups owned by the current user.
|
||||
func (as *AdminServer) APIGroupsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, gs, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroup returns details about the requested group.
|
||||
// If the group is not valid, APIGroup returns null.
|
||||
func (as *AdminServer) APIGroup(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting group"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Group deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
// Change this to get from URL and uid (don't bother with id in r.Body)
|
||||
g = models.Group{}
|
||||
err = json.NewDecoder(r.Body).Decode(&g)
|
||||
if g.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
g.ModifiedDate = time.Now().UTC()
|
||||
g.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutGroup(&g)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIGroupSummary returns a summary of the groups owned by the current user.
|
||||
func (as *AdminServer) APIGroupSummary(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
g, err := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, g, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APITemplates handles the functionality for the /api/templates endpoint
|
||||
func (as *AdminServer) APITemplates(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ts, err := models.GetTemplates(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
//POST: Create a new template and return it as JSON
|
||||
case r.Method == "POST":
|
||||
t := models.Template{}
|
||||
// Put the request into a template
|
||||
err := json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err = models.GetTemplateByName(t.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template name already in use"}, http.StatusConflict)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostTemplate(&t)
|
||||
if err == models.ErrTemplateNameNotSpecified {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err == models.ErrTemplateMissingParameter {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error inserting template into database"}, http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APITemplate handles the functions for the /api/templates/:id endpoint
|
||||
func (as *AdminServer) APITemplate(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
t, err := models.GetTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Template not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteTemplate(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting template"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Template deleted successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
t = models.Template{}
|
||||
err = json.NewDecoder(r.Body).Decode(&t)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if t.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and template_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
t.ModifiedDate = time.Now().UTC()
|
||||
t.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutTemplate(&t)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, t, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIPages handles requests for the /api/pages/ endpoint
|
||||
func (as *AdminServer) APIPages(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ps, err := models.GetPages(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ps, http.StatusOK)
|
||||
//POST: Create a new page and return it as JSON
|
||||
case r.Method == "POST":
|
||||
p := models.Page{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetPageByName(p.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APIPage contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a Page object
|
||||
func (as *AdminServer) APIPage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
p, err := models.GetPage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Page not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeletePage(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Page Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
p = models.Page{}
|
||||
err = json.NewDecoder(r.Body).Decode(&p)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if p.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:page_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
p.ModifiedDate = time.Now().UTC()
|
||||
p.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutPage(&p)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page: " + err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, p, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APISendingProfiles handles requests for the /api/smtp/ endpoint
|
||||
func (as *AdminServer) APISendingProfiles(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
ss, err := models.GetSMTPs(ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
JSONResponse(w, ss, http.StatusOK)
|
||||
//POST: Create a new SMTP and return it as JSON
|
||||
case r.Method == "POST":
|
||||
s := models.SMTP{}
|
||||
// Put the request into a page
|
||||
err := json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Invalid request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Check to make sure the name is unique
|
||||
_, err = models.GetSMTPByName(s.Name, ctx.Get(r, "user_id").(int64))
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP name already in use"}, http.StatusConflict)
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PostSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
// APISendingProfile contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
|
||||
// of a SMTP object
|
||||
func (as *AdminServer) APISendingProfile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
s, err := models.GetSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "SMTP not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteSMTP(id, ctx.Get(r, "user_id").(int64))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error deleting SMTP"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "SMTP Deleted Successfully"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
s = models.SMTP{}
|
||||
err = json.NewDecoder(r.Body).Decode(&s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if s.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "/:id and /:smtp_id mismatch"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = s.Validate()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.ModifiedDate = time.Now().UTC()
|
||||
s.UserId = ctx.Get(r, "user_id").(int64)
|
||||
err = models.PutSMTP(&s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error updating page"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, s, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// APIImportGroup imports a CSV of group members
|
||||
func (as *AdminServer) APIImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||
ts, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APIImportEmail allows for the importing of email.
|
||||
// Returns a Message object
|
||||
func (as *AdminServer) APIImportEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ir := struct {
|
||||
Content string `json:"content"`
|
||||
ConvertLinks bool `json:"convert_links"`
|
||||
}{}
|
||||
err := json.NewDecoder(r.Body).Decode(&ir)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e, err := email.NewEmailFromReader(strings.NewReader(ir.Content))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// If the user wants to convert links to point to
|
||||
// the landing page, let's make it happen by changing up
|
||||
// e.HTML
|
||||
if ir.ConvertLinks {
|
||||
d, err := goquery.NewDocumentFromReader(bytes.NewReader(e.HTML))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d.Find("a").Each(func(i int, a *goquery.Selection) {
|
||||
a.SetAttr("href", "{{.URL}}")
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
e.HTML = []byte(h)
|
||||
}
|
||||
er := emailResponse{
|
||||
Subject: e.Subject,
|
||||
Text: string(e.Text),
|
||||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APIImportSite allows for the importing of HTML from a website
|
||||
// Without "include_resources" set, it will merely place a "base" tag
|
||||
// so that all resources can be loaded relative to the given URL.
|
||||
func (as *AdminServer) APIImportSite(w http.ResponseWriter, r *http.Request) {
|
||||
cr := cloneRequest{}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cr)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err = cr.validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Get(cr.URL)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Insert the base href tag to better handle relative resources
|
||||
d, err := goquery.NewDocumentFromResponse(resp)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Assuming we don't want to include resources, we'll need a base href
|
||||
if d.Find("head base").Length() == 0 {
|
||||
d.Find("head").PrependHtml(fmt.Sprintf("<base href=\"%s\">", cr.URL))
|
||||
}
|
||||
forms := d.Find("form")
|
||||
forms.Each(func(i int, f *goquery.Selection) {
|
||||
// We'll want to store where we got the form from
|
||||
// (the current URL)
|
||||
url := f.AttrOr("action", cr.URL)
|
||||
if !strings.HasPrefix(url, "http") {
|
||||
url = fmt.Sprintf("%s%s", cr.URL, url)
|
||||
}
|
||||
f.PrependHtml(fmt.Sprintf("<input type=\"hidden\" name=\"__original_url\" value=\"%s\"/>", url))
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// APISendTestEmail sends a test email using the template name
|
||||
// and Target given.
|
||||
func (as *AdminServer) APISendTestEmail(w http.ResponseWriter, r *http.Request) {
|
||||
s := &models.EmailRequest{
|
||||
ErrorChan: make(chan error),
|
||||
UserId: ctx.Get(r, "user_id").(int64),
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
storeRequest := false
|
||||
|
||||
// If a Template is not specified use a default
|
||||
if s.Template.Name == "" {
|
||||
//default message body
|
||||
text := "It works!\n\nThis is an email letting you know that your gophish\nconfiguration was successful.\n" +
|
||||
"Here are the details:\n\nWho you sent from: {{.From}}\n\nWho you sent to: \n" +
|
||||
"{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" +
|
||||
"{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" +
|
||||
"{{if .Position}} Position: {{.Position}}\n{{end}}" +
|
||||
"\nNow go send some phish!"
|
||||
t := models.Template{
|
||||
Subject: "Default Email from Gophish",
|
||||
Text: text,
|
||||
}
|
||||
s.Template = t
|
||||
} else {
|
||||
// Get the Template requested by name
|
||||
s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"template": s.Template.Name,
|
||||
}).Error("Template does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrTemplateNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.TemplateId = s.Template.Id
|
||||
// We'll only save the test request to the database if there is a
|
||||
// user-specified template to use.
|
||||
storeRequest = true
|
||||
}
|
||||
|
||||
if s.Page.Name != "" {
|
||||
s.Page, err = models.GetPageByName(s.Page.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"page": s.Page.Name,
|
||||
}).Error("Page does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.PageId = s.Page.Id
|
||||
}
|
||||
|
||||
// If a complete sending profile is provided use it
|
||||
if err := s.SMTP.Validate(); err != nil {
|
||||
// Otherwise get the SMTP requested by name
|
||||
smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId)
|
||||
// If the Sending Profile doesn't exist, let's err on the side
|
||||
// of caution and assume that the validation failure was more important.
|
||||
if lookupErr != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.SMTP = smtp
|
||||
}
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
|
||||
// Validate the given request
|
||||
if err = s.Validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the request if this wasn't the default template
|
||||
if storeRequest {
|
||||
err = models.PostEmailRequest(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Send the test email
|
||||
err = as.worker.SendTestEmail(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// JSONResponse attempts to set the status code, c, and marshal the given interface, d, into a response that
|
||||
// is written to the given ResponseWriter.
|
||||
func JSONResponse(w http.ResponseWriter, d interface{}, c int) {
|
||||
dj, err := json.MarshalIndent(d, "", " ")
|
||||
if err != nil {
|
||||
http.Error(w, "Error creating JSON response", http.StatusInternalServerError)
|
||||
log.Error(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(c)
|
||||
fmt.Fprintf(w, "%s", dj)
|
||||
}
|
||||
|
||||
type cloneRequest struct {
|
||||
URL string `json:"url"`
|
||||
IncludeResources bool `json:"include_resources"`
|
||||
}
|
||||
|
||||
func (cr *cloneRequest) validate() error {
|
||||
if cr.URL == "" {
|
||||
return errors.New("No URL Specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloneResponse struct {
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
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
|
||||
admin models.User
|
||||
}
|
||||
|
||||
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
|
||||
s.admin = u
|
||||
// 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)
|
||||
}
|
||||
// Cleanup all users except the original admin
|
||||
users, _ := models.GetUsers()
|
||||
for _, user := range users {
|
||||
if user.Id == 1 {
|
||||
continue
|
||||
}
|
||||
err := models.DeleteUser(user.Id)
|
||||
s.Nil(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APISuite) SetupTest() {
|
||||
// Add a group
|
||||
group := models.Group{Name: "Test Group"}
|
||||
group.Targets = []models.Target{
|
||||
models.Target{BaseRecipient: models.BaseRecipient{Email: "test1@example.com", FirstName: "First", LastName: "Example"}},
|
||||
models.Target{BaseRecipient: models.BaseRecipient{Email: "test2@example.com", FirstName: "Second", LastName: "Example"}},
|
||||
}
|
||||
group.UserId = 1
|
||||
models.PostGroup(&group)
|
||||
|
||||
// Add a template
|
||||
t := models.Template{Name: "Test Template"}
|
||||
t.Subject = "Test subject"
|
||||
t.Text = "Text text"
|
||||
t.HTML = "<html>Test</html>"
|
||||
t.UserId = 1
|
||||
models.PostTemplate(&t)
|
||||
|
||||
// Add a landing page
|
||||
p := models.Page{Name: "Test Page"}
|
||||
p.HTML = "<html>Test</html>"
|
||||
p.UserId = 1
|
||||
models.PostPage(&p)
|
||||
|
||||
// Add a sending profile
|
||||
smtp := models.SMTP{Name: "Test Page"}
|
||||
smtp.UserId = 1
|
||||
smtp.Host = "example.com"
|
||||
smtp.FromAddress = "test@test.com"
|
||||
models.PostSMTP(&smtp)
|
||||
|
||||
// Setup and "launch" our campaign
|
||||
// Set the status such that no emails are attempted
|
||||
c := models.Campaign{Name: "Test campaign"}
|
||||
c.UserId = 1
|
||||
c.Template = t
|
||||
c.Page = p
|
||||
c.SMTP = smtp
|
||||
c.Groups = []models.Group{group}
|
||||
models.PostCampaign(&c, c.UserId)
|
||||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestSiteImportBaseHref() {
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
hr := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
defer ts.Close()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/import/site",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
{
|
||||
"url" : "%s",
|
||||
"include_resources" : false
|
||||
}
|
||||
`, ts.URL))))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
s.apiServer.ImportSite(response, req)
|
||||
cs := cloneResponse{}
|
||||
err := json.NewDecoder(response.Body).Decode(&cs)
|
||||
s.Nil(err)
|
||||
s.Equal(cs.HTML, hr)
|
||||
}
|
||||
|
||||
func TestAPISuite(t *testing.T) {
|
||||
suite.Run(t, new(APISuite))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/jordan-wright/email"
|
||||
)
|
||||
|
||||
type cloneRequest struct {
|
||||
URL string `json:"url"`
|
||||
IncludeResources bool `json:"include_resources"`
|
||||
}
|
||||
|
||||
func (cr *cloneRequest) validate() error {
|
||||
if cr.URL == "" {
|
||||
return errors.New("No URL Specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type cloneResponse struct {
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html"`
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
|
||||
// ImportGroup imports a CSV of group members
|
||||
func (as *Server) ImportGroup(w http.ResponseWriter, r *http.Request) {
|
||||
ts, err := util.ParseCSV(r)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error parsing CSV"}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, ts, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportEmail allows for the importing of email.
|
||||
// Returns a Message object
|
||||
func (as *Server) ImportEmail(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ir := struct {
|
||||
Content string `json:"content"`
|
||||
ConvertLinks bool `json:"convert_links"`
|
||||
}{}
|
||||
err := json.NewDecoder(r.Body).Decode(&ir)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e, err := email.NewEmailFromReader(strings.NewReader(ir.Content))
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// If the user wants to convert links to point to
|
||||
// the landing page, let's make it happen by changing up
|
||||
// e.HTML
|
||||
if ir.ConvertLinks {
|
||||
d, err := goquery.NewDocumentFromReader(bytes.NewReader(e.HTML))
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
d.Find("a").Each(func(i int, a *goquery.Selection) {
|
||||
a.SetAttr("href", "{{.URL}}")
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
e.HTML = []byte(h)
|
||||
}
|
||||
er := emailResponse{
|
||||
Subject: e.Subject,
|
||||
Text: string(e.Text),
|
||||
HTML: string(e.HTML),
|
||||
}
|
||||
JSONResponse(w, er, http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// ImportSite allows for the importing of HTML from a website
|
||||
// Without "include_resources" set, it will merely place a "base" tag
|
||||
// so that all resources can be loaded relative to the given URL.
|
||||
func (as *Server) ImportSite(w http.ResponseWriter, r *http.Request) {
|
||||
cr := cloneRequest{}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(&cr)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err = cr.validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Get(cr.URL)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Insert the base href tag to better handle relative resources
|
||||
d, err := goquery.NewDocumentFromResponse(resp)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Assuming we don't want to include resources, we'll need a base href
|
||||
if d.Find("head base").Length() == 0 {
|
||||
d.Find("head").PrependHtml(fmt.Sprintf("<base href=\"%s\">", cr.URL))
|
||||
}
|
||||
forms := d.Find("form")
|
||||
forms.Each(func(i int, f *goquery.Selection) {
|
||||
// We'll want to store where we got the form from
|
||||
// (the current URL)
|
||||
url := f.AttrOr("action", cr.URL)
|
||||
if !strings.HasPrefix(url, "http") {
|
||||
url = fmt.Sprintf("%s%s", cr.URL, url)
|
||||
}
|
||||
f.PrependHtml(fmt.Sprintf("<input type=\"hidden\" name=\"__original_url\" value=\"%s\"/>", url))
|
||||
})
|
||||
h, err := d.Html()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cs := cloneResponse{HTML: h}
|
||||
JSONResponse(w, cs, http.StatusOK)
|
||||
return
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
)
|
||||
|
||||
// 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 = util.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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
"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("/users/", mid.Use(as.Users, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/users/{id:[0-9]+}", mid.Use(as.User))
|
||||
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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
||||
var ErrUsernameTaken = errors.New("Username already taken")
|
||||
|
||||
// ErrEmptyUsername is thrown when a user attempts to register a username that is taken.
|
||||
var ErrEmptyUsername = errors.New("No username provided")
|
||||
|
||||
// ErrEmptyRole is throws when no role is provided when creating or modifying a user.
|
||||
var ErrEmptyRole = errors.New("No role specified")
|
||||
|
||||
// ErrInsufficientPermission is thrown when a user attempts to change an
|
||||
// attribute (such as the role) for which they don't have permission.
|
||||
var ErrInsufficientPermission = errors.New("Permission denied")
|
||||
|
||||
// userRequest is the payload which represents the creation of a new user.
|
||||
type userRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
func (ur *userRequest) Validate(existingUser *models.User) error {
|
||||
switch {
|
||||
case ur.Username == "":
|
||||
return ErrEmptyUsername
|
||||
case ur.Role == "":
|
||||
return ErrEmptyRole
|
||||
}
|
||||
// Verify that the username isn't already taken. We consider two cases:
|
||||
// * We're creating a new user, in which case any match is a conflict
|
||||
// * We're modifying a user, in which case any match with a different ID is
|
||||
// a conflict.
|
||||
possibleConflict, err := models.GetUserByUsername(ur.Username)
|
||||
if err == nil {
|
||||
if existingUser == nil {
|
||||
return ErrUsernameTaken
|
||||
}
|
||||
if possibleConflict.Id != existingUser.Id {
|
||||
return ErrUsernameTaken
|
||||
}
|
||||
}
|
||||
// If we have an error which is not simply indicating that no user was found, report it
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Users contains functions to retrieve a list of existing users or create a
|
||||
// new user. Users with the ModifySystem permissions can view and create users.
|
||||
func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
us, err := models.GetUsers()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, us, http.StatusOK)
|
||||
return
|
||||
case r.Method == "POST":
|
||||
ur := &userRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(ur)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = ur.Validate(nil)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if ur.Password == "" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
role, err := models.GetRoleBySlug(ur.Role)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user := models.User{
|
||||
Username: ur.Username,
|
||||
Hash: hash,
|
||||
ApiKey: util.GenerateSecureKey(),
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
}
|
||||
err = models.PutUser(&user)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, user, http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// User contains functions to retrieve or delete a single user. Users with
|
||||
// the ModifySystem permission can view and modify any user. Otherwise, users
|
||||
// may only view or delete their own account.
|
||||
func (as *Server) User(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||
// If the user doesn't have ModifySystem permissions, we need to verify
|
||||
// that they're only taking action on their account.
|
||||
currentUser := ctx.Get(r, "user").(models.User)
|
||||
hasSystem, err := currentUser.HasPermission(models.PermissionModifySystem)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !hasSystem && currentUser.Id != id {
|
||||
JSONResponse(w, models.Response{Success: false, Message: http.StatusText(http.StatusForbidden)}, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
existingUser, err := models.GetUser(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "User not found"}, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
JSONResponse(w, existingUser, http.StatusOK)
|
||||
case r.Method == "DELETE":
|
||||
err = models.DeleteUser(id)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Infof("Deleted user account for %s", existingUser.Username)
|
||||
JSONResponse(w, models.Response{Success: true, Message: "User deleted Successfully!"}, http.StatusOK)
|
||||
case r.Method == "PUT":
|
||||
ur := &userRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(ur)
|
||||
if err != nil {
|
||||
log.Errorf("error decoding user request: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = ur.Validate(&existingUser)
|
||||
if err != nil {
|
||||
log.Errorf("invalid user request received: %v", err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
existingUser.Username = ur.Username
|
||||
// Only users with the ModifySystem permission are able to update a
|
||||
// user's role. This prevents a privilege escalation letting users
|
||||
// upgrade their own account.
|
||||
if !hasSystem && ur.Role != existingUser.Role.Slug {
|
||||
JSONResponse(w, models.Response{Success: false, Message: ErrInsufficientPermission.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
role, err := models.GetRoleBySlug(ur.Role)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// If our user is trying to change the role of an admin, we need to
|
||||
// ensure that it isn't the last user account with the Admin role.
|
||||
if existingUser.Role.Slug == models.RoleAdmin && existingUser.Role.ID != role.ID {
|
||||
err = models.EnsureEnoughAdmins()
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
existingUser.Role = role
|
||||
existingUser.RoleID = role.ID
|
||||
// We don't force the password to be provided, since it may be an admin
|
||||
// managing the user's account, and making a simple change like
|
||||
// updating the username or role. However, if it _is_ provided, we'll
|
||||
// update the stored hash.
|
||||
//
|
||||
// Note that we don't force the current password to be provided. The
|
||||
// assumption here is that the API key is a proper bearer token proving
|
||||
// authenticated access to the account.
|
||||
if ur.Password != "" {
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
existingUser.Hash = hash
|
||||
}
|
||||
err = models.PutUser(&existingUser)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, existingUser, http.StatusOK)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
func (s *APISuite) createUnpriviledgedUser(slug string) *models.User {
|
||||
role, err := models.GetRoleBySlug(slug)
|
||||
s.Nil(err)
|
||||
unauthorizedUser := &models.User{
|
||||
Username: "foo",
|
||||
Hash: "bar",
|
||||
ApiKey: "12345",
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
}
|
||||
err = models.PutUser(unauthorizedUser)
|
||||
s.Nil(err)
|
||||
return unauthorizedUser
|
||||
}
|
||||
|
||||
func (s *APISuite) TestGetUsers() {
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/users", nil)
|
||||
r = ctx.Set(r, "user", s.admin)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.Users(w, r)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
|
||||
got := []models.User{}
|
||||
err := json.NewDecoder(w.Body).Decode(&got)
|
||||
s.Nil(err)
|
||||
|
||||
// We only expect one user
|
||||
s.Equal(1, len(got))
|
||||
// And it should be the admin user
|
||||
s.Equal(s.admin.Id, got[0].Id)
|
||||
}
|
||||
|
||||
func (s *APISuite) TestCreateUser() {
|
||||
payload := &userRequest{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r = ctx.Set(r, "user", s.admin)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.Users(w, r)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
|
||||
got := &models.User{}
|
||||
err = json.NewDecoder(w.Body).Decode(got)
|
||||
s.Nil(err)
|
||||
s.Equal(got.Username, payload.Username)
|
||||
s.Equal(got.Role.Slug, payload.Role)
|
||||
}
|
||||
|
||||
// TestModifyUser tests that a user with the appropriate access is able to
|
||||
// modify their username and password.
|
||||
func (s *APISuite) TestModifyUser() {
|
||||
unpriviledgedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
newPassword := "new-password"
|
||||
newUsername := "new-username"
|
||||
payload := userRequest{
|
||||
Username: newUsername,
|
||||
Password: newPassword,
|
||||
Role: unpriviledgedUser.Role.Slug,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
url := fmt.Sprintf("/api/users/%d", unpriviledgedUser.Id)
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unpriviledgedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
response := &models.User{}
|
||||
err = json.NewDecoder(w.Body).Decode(response)
|
||||
s.Nil(err)
|
||||
s.Equal(w.Code, http.StatusOK)
|
||||
s.Equal(response.Username, newUsername)
|
||||
got, err := models.GetUser(unpriviledgedUser.Id)
|
||||
s.Nil(err)
|
||||
s.Equal(response.Username, got.Username)
|
||||
s.Equal(newUsername, got.Username)
|
||||
err = bcrypt.CompareHashAndPassword([]byte(got.Hash), []byte(newPassword))
|
||||
s.Nil(err)
|
||||
}
|
||||
|
||||
// TestUnauthorizedListUsers ensures that users without the ModifySystem
|
||||
// permission are unable to list the users registered in Gophish.
|
||||
func (s *APISuite) TestUnauthorizedListUsers() {
|
||||
// First, let's create a standard user which doesn't
|
||||
// have ModifySystem permissions.
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
// We'll try to make a request to the various users API endpoints to
|
||||
// ensure that they fail. Previously, we could hit the handlers directly
|
||||
// but we need to go through the router for this test to ensure the
|
||||
// middleware gets applied.
|
||||
r := httptest.NewRequest(http.MethodGet, "/api/users/", nil)
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// TestUnauthorizedModifyUsers verifies that users without ModifySystem
|
||||
// permission (a "standard" user) can only get or modify their own information.
|
||||
func (s *APISuite) TestUnauthorizedGetUser() {
|
||||
// First, we'll make sure that a user with the "user" role is unable to
|
||||
// get the information of another user (in this case, the main admin).
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
url := fmt.Sprintf("/api/users/%d", s.admin.Id)
|
||||
r := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// TestUnauthorizedModifyRole ensures that users without the ModifySystem
|
||||
// privilege are unable to modify their own role, preventing a potential
|
||||
// privilege escalation issue.
|
||||
func (s *APISuite) TestUnauthorizedSetRole() {
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||
payload := &userRequest{
|
||||
Username: unauthorizedUser.Username,
|
||||
Role: models.RoleAdmin,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusBadRequest)
|
||||
response := &models.Response{}
|
||||
err = json.NewDecoder(w.Body).Decode(response)
|
||||
s.Nil(err)
|
||||
s.Equal(response.Message, ErrInsufficientPermission.Error())
|
||||
}
|
||||
|
||||
// TestModifyWithExistingUsername verifies that it's not possible to modify
|
||||
// an user's username to one which already exists.
|
||||
func (s *APISuite) TestModifyWithExistingUsername() {
|
||||
unauthorizedUser := s.createUnpriviledgedUser(models.RoleUser)
|
||||
payload := &userRequest{
|
||||
Username: s.admin.Username,
|
||||
Role: unauthorizedUser.Role.Slug,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
s.Nil(err)
|
||||
url := fmt.Sprintf("/api/users/%d", unauthorizedUser.Id)
|
||||
r := httptest.NewRequest(http.MethodPut, url, bytes.NewReader(body))
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", unauthorizedUser.ApiKey))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.apiServer.ServeHTTP(w, r)
|
||||
s.Equal(w.Code, http.StatusBadRequest)
|
||||
expected := &models.Response{
|
||||
Message: ErrUsernameTaken.Error(),
|
||||
Success: false,
|
||||
}
|
||||
got := &models.Response{}
|
||||
err = json.NewDecoder(w.Body).Decode(got)
|
||||
s.Nil(err)
|
||||
s.Equal(got.Message, expected.Message)
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SendTestEmail sends a test email using the template name
|
||||
// and Target given.
|
||||
func (as *Server) SendTestEmail(w http.ResponseWriter, r *http.Request) {
|
||||
s := &models.EmailRequest{
|
||||
ErrorChan: make(chan error),
|
||||
UserId: ctx.Get(r, "user_id").(int64),
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err := json.NewDecoder(r.Body).Decode(s)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: "Error decoding JSON Request"}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
storeRequest := false
|
||||
|
||||
// If a Template is not specified use a default
|
||||
if s.Template.Name == "" {
|
||||
//default message body
|
||||
text := "It works!\n\nThis is an email letting you know that your gophish\nconfiguration was successful.\n" +
|
||||
"Here are the details:\n\nWho you sent from: {{.From}}\n\nWho you sent to: \n" +
|
||||
"{{if .FirstName}} First Name: {{.FirstName}}\n{{end}}" +
|
||||
"{{if .LastName}} Last Name: {{.LastName}}\n{{end}}" +
|
||||
"{{if .Position}} Position: {{.Position}}\n{{end}}" +
|
||||
"\nNow go send some phish!"
|
||||
t := models.Template{
|
||||
Subject: "Default Email from Gophish",
|
||||
Text: text,
|
||||
}
|
||||
s.Template = t
|
||||
} else {
|
||||
// Get the Template requested by name
|
||||
s.Template, err = models.GetTemplateByName(s.Template.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"template": s.Template.Name,
|
||||
}).Error("Template does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrTemplateNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.TemplateId = s.Template.Id
|
||||
// We'll only save the test request to the database if there is a
|
||||
// user-specified template to use.
|
||||
storeRequest = true
|
||||
}
|
||||
|
||||
if s.Page.Name != "" {
|
||||
s.Page, err = models.GetPageByName(s.Page.Name, s.UserId)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
log.WithFields(logrus.Fields{
|
||||
"page": s.Page.Name,
|
||||
}).Error("Page does not exist")
|
||||
JSONResponse(w, models.Response{Success: false, Message: models.ErrPageNotFound.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.PageId = s.Page.Id
|
||||
}
|
||||
|
||||
// If a complete sending profile is provided use it
|
||||
if err := s.SMTP.Validate(); err != nil {
|
||||
// Otherwise get the SMTP requested by name
|
||||
smtp, lookupErr := models.GetSMTPByName(s.SMTP.Name, s.UserId)
|
||||
// If the Sending Profile doesn't exist, let's err on the side
|
||||
// of caution and assume that the validation failure was more important.
|
||||
if lookupErr != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
s.SMTP = smtp
|
||||
}
|
||||
s.FromAddress = s.SMTP.FromAddress
|
||||
|
||||
// Validate the given request
|
||||
if err = s.Validate(); err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Store the request if this wasn't the default template
|
||||
if storeRequest {
|
||||
err = models.PostEmailRequest(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Send the test email
|
||||
err = as.worker.SendTestEmail(s)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
JSONResponse(w, models.Response{Success: true, Message: "Email Sent"}, http.StatusOK)
|
||||
return
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
@ -103,52 +99,6 @@ func (s *ControllersSuite) SetupTest() {
|
|||
c.UpdateStatus(models.CampaignEmailsSent)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestRequireAPIKey() {
|
||||
resp, err := http.Post(fmt.Sprintf("%s/api/import/site", s.adminServer.URL), "application/json", nil)
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestInvalidAPIKey() {
|
||||
resp, err := http.Get(fmt.Sprintf("%s/api/groups/?api_key=%s", s.adminServer.URL, "bogus-api-key"))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestBearerToken() {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/groups/", s.adminServer.URL), nil)
|
||||
s.Nil(err)
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.apiKey))
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
s.Equal(resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TestSiteImportBaseHref() {
|
||||
h := "<html><head></head><body><img src=\"/test.png\"/></body></html>"
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, h)
|
||||
}))
|
||||
hr := fmt.Sprintf("<html><head><base href=\"%s\"/></head><body><img src=\"/test.png\"/>\n</body></html>", ts.URL)
|
||||
defer ts.Close()
|
||||
resp, err := http.Post(fmt.Sprintf("%s/api/import/site?api_key=%s", s.adminServer.URL, s.apiKey), "application/json",
|
||||
bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||
{
|
||||
"url" : "%s",
|
||||
"include_resources" : false
|
||||
}
|
||||
`, ts.URL))))
|
||||
s.Nil(err)
|
||||
defer resp.Body.Close()
|
||||
cs := cloneResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&cs)
|
||||
s.Nil(err)
|
||||
s.Equal(cs.HTML, hr)
|
||||
}
|
||||
|
||||
func (s *ControllersSuite) TearDownSuite() {
|
||||
// Tear down the admin and phishing servers
|
||||
s.adminServer.Close()
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/NYTimes/gziphandler"
|
||||
"github.com/gophish/gophish/config"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
|
@ -81,19 +82,18 @@ func WithContactAddress(addr string) PhishingServerOption {
|
|||
}
|
||||
|
||||
// Start launches the phishing server, listening on the configured address.
|
||||
func (ps *PhishingServer) Start() error {
|
||||
func (ps *PhishingServer) Start() {
|
||||
if ps.config.UseTLS {
|
||||
err := util.CheckAndCreateSSL(ps.config.CertPath, ps.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Starting phishing server at https://%s", ps.config.ListenURL)
|
||||
return ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath)
|
||||
log.Fatal(ps.server.ListenAndServeTLS(ps.config.CertPath, ps.config.KeyPath))
|
||||
}
|
||||
// If TLS isn't configured, just listen on HTTP
|
||||
log.Infof("Starting phishing server at http://%s", ps.config.ListenURL)
|
||||
return ps.server.ListenAndServe()
|
||||
log.Fatal(ps.server.ListenAndServe())
|
||||
}
|
||||
|
||||
// Shutdown attempts to gracefully shutdown the server.
|
||||
|
@ -160,6 +160,7 @@ func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// ReportHandler tracks emails as they are reported, updating the status for the given Result
|
||||
func (ps *PhishingServer) ReportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
r, err := setupContext(r)
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*") // To allow Chrome extensions (or other pages) to report a campaign without violating CORS
|
||||
if err != nil {
|
||||
// Log the error if it wasn't something we can safely ignore
|
||||
if err != ErrInvalidRequest && err != ErrCampaignComplete {
|
||||
|
@ -202,6 +203,7 @@ func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) {
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Server", config.ServerName) // Useful for checking if this is a GoPhish server (e.g. for campaign reporting plugins)
|
||||
var ptx models.PhishingTemplateContext
|
||||
// Check for a preview
|
||||
if preview, ok := ctx.Get(r, "result").(models.EmailRequest); ok {
|
||||
|
@ -299,7 +301,7 @@ func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Req
|
|||
SendDate: rs.SendDate,
|
||||
ContactAddress: ps.contactAddress,
|
||||
}
|
||||
JSONResponse(w, tr, http.StatusOK)
|
||||
api.JSONResponse(w, tr, http.StatusOK)
|
||||
}
|
||||
|
||||
// setupContext handles some of the administrative work around receiving a new
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
|
@ -64,7 +65,7 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
|
|||
}
|
||||
|
||||
// Start launches the admin server, listening on the configured address.
|
||||
func (as *AdminServer) Start() error {
|
||||
func (as *AdminServer) Start() {
|
||||
if as.worker != nil {
|
||||
go as.worker.Start()
|
||||
}
|
||||
|
@ -72,14 +73,13 @@ func (as *AdminServer) Start() error {
|
|||
err := util.CheckAndCreateSSL(as.config.CertPath, as.config.KeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Starting admin server at https://%s", as.config.ListenURL)
|
||||
return as.server.ListenAndServeTLS(as.config.CertPath, as.config.KeyPath)
|
||||
log.Fatal(as.server.ListenAndServeTLS(as.config.CertPath, as.config.KeyPath))
|
||||
}
|
||||
// If TLS isn't configured, just listen on HTTP
|
||||
log.Infof("Starting admin server at http://%s", as.config.ListenURL)
|
||||
return as.server.ListenAndServe()
|
||||
log.Fatal(as.server.ListenAndServe())
|
||||
}
|
||||
|
||||
// Shutdown attempts to gracefully shutdown the server.
|
||||
|
@ -94,53 +94,30 @@ func (as *AdminServer) Shutdown() error {
|
|||
func (as *AdminServer) registerRoutes() {
|
||||
router := mux.NewRouter()
|
||||
// Base Front-end routes
|
||||
router.HandleFunc("/", Use(as.Base, mid.RequireLogin))
|
||||
router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
|
||||
router.HandleFunc("/login", as.Login)
|
||||
router.HandleFunc("/logout", Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", Use(as.Templates, mid.RequireLogin))
|
||||
router.HandleFunc("/users", Use(as.Users, mid.RequireLogin))
|
||||
router.HandleFunc("/landing_pages", Use(as.LandingPages, mid.RequireLogin))
|
||||
router.HandleFunc("/sending_profiles", Use(as.SendingProfiles, mid.RequireLogin))
|
||||
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/register", Use(as.Register, mid.RequireLogin, mid.RequirePermission(models.PermissionModifySystem)))
|
||||
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||
router.HandleFunc("/groups", mid.Use(as.Groups, mid.RequireLogin))
|
||||
router.HandleFunc("/landing_pages", mid.Use(as.LandingPages, mid.RequireLogin))
|
||||
router.HandleFunc("/sending_profiles", mid.Use(as.SendingProfiles, mid.RequireLogin))
|
||||
router.HandleFunc("/settings", mid.Use(as.Settings, mid.RequireLogin))
|
||||
router.HandleFunc("/users", mid.Use(as.UserManagement, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
// 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/")))
|
||||
|
||||
// Setup CSRF Protection
|
||||
csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()),
|
||||
csrfHandler := csrf.Protect([]byte(util.GenerateSecureKey()),
|
||||
csrf.FieldName("csrf_token"),
|
||||
csrf.Secure(as.config.UseTLS))
|
||||
adminHandler := csrfHandler(router)
|
||||
adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||
adminHandler = mid.Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
|
||||
|
||||
// Setup GZIP compression
|
||||
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
|
||||
|
@ -151,15 +128,6 @@ func (as *AdminServer) registerRoutes() {
|
|||
as.server.Handler = adminHandler
|
||||
}
|
||||
|
||||
// Use allows us to stack middleware to process the request
|
||||
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
|
||||
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
|
||||
for _, m := range mid {
|
||||
handler = m(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
type templateParams struct {
|
||||
Title string
|
||||
Flashes []interface{}
|
||||
|
@ -182,42 +150,6 @@ func newTemplateParams(r *http.Request) templateParams {
|
|||
}
|
||||
}
|
||||
|
||||
// Register creates a new user
|
||||
func (as *AdminServer) Register(w http.ResponseWriter, r *http.Request) {
|
||||
// If it is a post request, attempt to register the account
|
||||
// Now that we are all registered, we can log the user in
|
||||
params := templateParams{Title: "Register", Token: csrf.Token(r)}
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/register.html", "templates/flashes.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
//Attempt to register
|
||||
succ, err := auth.Register(r)
|
||||
//If we've registered, redirect to the login page
|
||||
if succ {
|
||||
Flash(w, r, "success", "Registration successful!")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/login", 302)
|
||||
return
|
||||
}
|
||||
// Check the error
|
||||
m := err.Error()
|
||||
log.Error(err)
|
||||
Flash(w, r, "danger", m)
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/register", 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Base handles the default path and template execution
|
||||
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
|
@ -246,11 +178,11 @@ func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) {
|
|||
getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Users handles the default path and template execution
|
||||
func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) {
|
||||
// Groups handles the default path and template execution
|
||||
func (as *AdminServer) Groups(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Users & Groups"
|
||||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||
getTemplate(w, "groups").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// LandingPages handles the default path and template execution
|
||||
|
@ -280,19 +212,27 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
// UserManagement is an admin-only handler that allows for the registration
|
||||
// and management of user accounts within Gophish.
|
||||
func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "User Management"
|
||||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Login handles the authentication flow for a user. If credentials are valid,
|
||||
// a session is created
|
||||
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
#!/bin/bash
|
||||
|
||||
# set config for admin_server
|
||||
if [ -n "${ADMIN_LISTEN_URL+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_LISTEN_URL "${ADMIN_LISTEN_URL}" \
|
||||
'.admin_server.listen_url = $ADMIN_LISTEN_URL' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_USE_TLS+set}" ] ; then
|
||||
jq -r \
|
||||
--argjson ADMIN_USE_TLS "${ADMIN_USE_TLS}" \
|
||||
'.admin_server.use_tls = $ADMIN_USE_TLS' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_CERT_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_CERT_PATH "${ADMIN_CERT_PATH}" \
|
||||
'.admin_server.cert_path = $ADMIN_CERT_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${ADMIN_KEY_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg ADMIN_KEY_PATH "${ADMIN_KEY_PATH}" \
|
||||
'.admin_server.key_path = $ADMIN_KEY_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
# set config for phish_server
|
||||
if [ -n "${PHISH_LISTEN_URL+set}" ] ; then
|
||||
jq -r \
|
||||
--arg PHISH_LISTEN_URL "${PHISH_LISTEN_URL}" \
|
||||
'.phish_server.listen_url = $PHISH_LISTEN_URL' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_USE_TLS+set}" ] ; then
|
||||
jq -r \
|
||||
--argjson PHISH_USE_TLS "${PHISH_USE_TLS}" \
|
||||
'.phish_server.use_tls = $PHISH_USE_TLS' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_CERT_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg PHISH_CERT_PATH "${PHISH_CERT_PATH}" \
|
||||
'.phish_server.cert_path = $PHISH_CERT_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
if [ -n "${PHISH_KEY_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg PHISH_KEY_PATH "${PHISH_KEY_PATH}" \
|
||||
'.phish_server.key_path = $PHISH_KEY_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
# set contact_address
|
||||
if [ -n "${CONTACT_ADDRESS+set}" ] ; then
|
||||
jq -r \
|
||||
--arg CONTACT_ADDRESS "${CONTACT_ADDRESS}" \
|
||||
'.contact_address = $CONTACT_ADDRESS' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
if [ -n "${DB_FILE_PATH+set}" ] ; then
|
||||
jq -r \
|
||||
--arg DB_FILE_PATH "${DB_FILE_PATH}" \
|
||||
'.db_path = $DB_FILE_PATH' config.json > config.json.tmp && \
|
||||
cat config.json.tmp > config.json
|
||||
fi
|
||||
|
||||
echo "Runtime configuration: "
|
||||
cat config.json
|
||||
|
||||
# start gophish
|
||||
./gophish
|
|
@ -32,10 +32,10 @@ import (
|
|||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/controllers"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
||||
|
@ -94,7 +94,7 @@ func main() {
|
|||
}
|
||||
adminConfig := conf.AdminConf
|
||||
adminServer := controllers.NewAdminServer(adminConfig, adminOptions...)
|
||||
auth.Store.Options.Secure = adminConfig.UseTLS
|
||||
middleware.Store.Options.Secure = adminConfig.UseTLS
|
||||
|
||||
phishConfig := conf.PhishConf
|
||||
phishServer := controllers.NewPhishingServer(phishConfig)
|
||||
|
|
39
gulpfile.js
39
gulpfile.js
|
@ -9,16 +9,16 @@ var gulp = require('gulp'),
|
|||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
cleanCSS = require('gulp-clean-css'),
|
||||
babel = require('gulp-babel'),
|
||||
|
||||
js_directory = 'static/js/src/',
|
||||
css_directory = 'static/css/',
|
||||
vendor_directory = js_directory + 'vendor/',
|
||||
app_directory = js_directory + 'app/**/*.js',
|
||||
app_directory = js_directory + 'app/',
|
||||
dest_js_directory = 'static/js/dist/',
|
||||
dest_css_directory = 'static/css/dist/';
|
||||
|
||||
gulp.task('vendorjs', function () {
|
||||
// Vendor minifying / concat
|
||||
vendorjs = function () {
|
||||
return gulp.src([
|
||||
vendor_directory + 'jquery.js',
|
||||
vendor_directory + 'bootstrap.min.js',
|
||||
|
@ -46,11 +46,22 @@ gulp.task('vendorjs', function () {
|
|||
}))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest(dest_js_directory));
|
||||
})
|
||||
}
|
||||
|
||||
gulp.task('scripts', function () {
|
||||
// Gophish app files
|
||||
gulp.src(app_directory)
|
||||
scripts = function () {
|
||||
// Gophish app files - non-ES6
|
||||
return gulp.src([
|
||||
app_directory + 'autocomplete.js',
|
||||
app_directory + 'campaign_results.js',
|
||||
app_directory + 'campaigns.js',
|
||||
app_directory + 'dashboard.js',
|
||||
app_directory + 'groups.js',
|
||||
app_directory + 'landing_pages.js',
|
||||
app_directory + 'sending_profiles.js',
|
||||
app_directory + 'settings.js',
|
||||
app_directory + 'templates.js',
|
||||
app_directory + 'gophish.js',
|
||||
])
|
||||
.pipe(rename({
|
||||
suffix: '.min'
|
||||
}))
|
||||
|
@ -58,9 +69,9 @@ gulp.task('scripts', function () {
|
|||
console.log(e);
|
||||
}))
|
||||
.pipe(gulp.dest(dest_js_directory + 'app/'));
|
||||
})
|
||||
}
|
||||
|
||||
gulp.task('styles', function () {
|
||||
styles = function () {
|
||||
return gulp.src([
|
||||
css_directory + 'bootstrap.min.css',
|
||||
css_directory + 'main.css',
|
||||
|
@ -80,8 +91,10 @@ gulp.task('styles', function () {
|
|||
}))
|
||||
.pipe(concat('gophish.css'))
|
||||
.pipe(gulp.dest(dest_css_directory));
|
||||
})
|
||||
}
|
||||
|
||||
gulp.task('build', ['vendorjs', 'scripts', 'styles']);
|
||||
|
||||
gulp.task('default', ['build']);
|
||||
exports.vendorjs = vendorjs
|
||||
exports.scripts = scripts
|
||||
exports.styles = styles
|
||||
exports.build = gulp.parallel(vendorjs, scripts, styles)
|
||||
exports.default = exports.build
|
|
@ -6,7 +6,6 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/csrf"
|
||||
|
@ -31,6 +30,15 @@ func CSRFExceptions(handler http.Handler) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// Use allows us to stack middleware to process the request
|
||||
// Example taken from https://github.com/gorilla/mux/pull/36#issuecomment-25849172
|
||||
func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) http.HandlerFunc {
|
||||
for _, m := range mid {
|
||||
handler = m(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// GetContext wraps each request in a function which fills in the context for a given request.
|
||||
// This includes setting the User and Session keys and values as necessary for use in later functions.
|
||||
func GetContext(handler http.Handler) http.HandlerFunc {
|
||||
|
@ -43,7 +51,7 @@ func GetContext(handler http.Handler) http.HandlerFunc {
|
|||
}
|
||||
// Set the context appropriately here.
|
||||
// Set the session
|
||||
session, _ := auth.Store.Get(r, "gophish")
|
||||
session, _ := Store.Get(r, "gophish")
|
||||
// Put the session in the context so that we can
|
||||
// reuse the values in different handlers
|
||||
r = ctx.Set(r, "session", session)
|
||||
|
@ -107,11 +115,12 @@ func RequireLogin(handler http.Handler) http.HandlerFunc {
|
|||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if u := ctx.Get(r, "user"); u != nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
// init registers the necessary models to be saved in the session later
|
||||
func init() {
|
||||
gob.Register(&models.User{})
|
||||
gob.Register(&models.Flash{})
|
||||
Store.Options.HttpOnly = true
|
||||
// This sets the maxAge to 5 days for all cookies
|
||||
Store.MaxAge(86400 * 5)
|
||||
}
|
||||
|
||||
// Store contains the session information for the request
|
||||
var Store = sessions.NewCookieStore(
|
||||
[]byte(securecookie.GenerateRandomKey(64)), //Signing key
|
||||
[]byte(securecookie.GenerateRandomKey(32)))
|
|
@ -1,12 +1,15 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -162,6 +165,14 @@ func (m *MailLog) Generate(msg *gomail.Message) error {
|
|||
if conf.ContactAddress != "" {
|
||||
msg.SetHeader("X-Gophish-Contact", conf.ContactAddress)
|
||||
}
|
||||
|
||||
// Add Message-Id header as described in RFC 2822.
|
||||
messageID, err := m.generateMessageID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.SetHeader("Message-Id", messageID)
|
||||
|
||||
// Parse the customHeader templates
|
||||
for _, header := range c.SMTP.Headers {
|
||||
key, err := ExecuteTemplate(header.Key, ptx)
|
||||
|
@ -261,3 +272,30 @@ func LockMailLogs(ms []*MailLog, lock bool) error {
|
|||
func UnlockAllMailLogs() error {
|
||||
return db.Model(&MailLog{}).Update("processing", false).Error
|
||||
}
|
||||
|
||||
var maxBigInt = big.NewInt(math.MaxInt64)
|
||||
|
||||
// generateMessageID generates and returns a string suitable for an RFC 2822
|
||||
// compliant Message-ID, e.g.:
|
||||
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
|
||||
//
|
||||
// The following parameters are used to generate a Message-ID:
|
||||
// - The nanoseconds since Epoch
|
||||
// - The calling PID
|
||||
// - A cryptographically random int64
|
||||
// - The sending hostname
|
||||
func (m *MailLog) generateMessageID() (string, error) {
|
||||
t := time.Now().UnixNano()
|
||||
pid := os.Getpid()
|
||||
rint, err := rand.Int(rand.Reader, maxBigInt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := os.Hostname()
|
||||
// If we can't get the hostname, we'll use localhost
|
||||
if err != nil {
|
||||
h = "localhost.localdomain"
|
||||
}
|
||||
msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
|
||||
return msgid, nil
|
||||
}
|
||||
|
|
|
@ -4,10 +4,14 @@ import (
|
|||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
|
||||
"bitbucket.org/liamstask/goose/lib/goose"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Blank import needed to import mysql
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/gophish/gophish/config"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -17,6 +21,8 @@ import (
|
|||
var db *gorm.DB
|
||||
var conf *config.Config
|
||||
|
||||
const MaxDatabaseConnectionAttempts int = 10
|
||||
|
||||
const (
|
||||
CampaignInProgress string = "In progress"
|
||||
CampaignQueued string = "Queued"
|
||||
|
@ -93,8 +99,45 @@ func Setup(c *config.Config) error {
|
|||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Register certificates for tls encrypted db connections
|
||||
if conf.DBSSLCaPath != "" {
|
||||
switch conf.DBName {
|
||||
case "mysql":
|
||||
rootCertPool := x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(conf.DBSSLCaPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
|
||||
log.Error("Failed to append PEM.")
|
||||
return err
|
||||
}
|
||||
mysql.RegisterTLSConfig("ssl_ca", &tls.Config{
|
||||
RootCAs: rootCertPool,
|
||||
})
|
||||
// Default database is sqlite3, which supports no tls, as connection
|
||||
// is file based
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Open our database connection
|
||||
i := 0
|
||||
for {
|
||||
db, err = gorm.Open(conf.DBName, conf.DBPath)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if err != nil && i >= MaxDatabaseConnectionAttempts {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
i += 1
|
||||
log.Warn("waiting for database to be up...")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
db.LogMode(false)
|
||||
db.SetLogger(log.Logger)
|
||||
db.DB().SetMaxOpenConns(1)
|
||||
|
|
|
@ -48,7 +48,7 @@ const (
|
|||
// Role represents a user role within Gophish. Each user has a single role
|
||||
// which maps to a set of permissions.
|
||||
type Role struct {
|
||||
ID int64 `json:"id"`
|
||||
ID int64 `json:"-"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
|
|
|
@ -218,6 +218,7 @@ func PutSMTP(s *SMTP) error {
|
|||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
// Save custom headers
|
||||
for i := range s.Headers {
|
||||
s.Headers[i].SMTPId = s.Id
|
||||
err := db.Save(&s.Headers[i]).Error
|
||||
|
|
|
@ -45,7 +45,10 @@ func NewPhishingTemplateContext(ctx TemplateContext, r BaseRecipient, rid string
|
|||
|
||||
// For the base URL, we'll reset the the path and the query
|
||||
// This will create a URL in the form of http://example.com
|
||||
baseURL, _ := url.Parse(templateURL)
|
||||
baseURL, err:= url.Parse(templateURL)
|
||||
if err != nil {
|
||||
return PhishingTemplateContext{}, err
|
||||
}
|
||||
baseURL.Path = ""
|
||||
baseURL.RawQuery = ""
|
||||
|
||||
|
|
117
models/user.go
117
models/user.go
|
@ -1,5 +1,16 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
)
|
||||
|
||||
// ErrModifyingOnlyAdmin occurs when there is an attempt to modify the only
|
||||
// user account with the Admin role in such a way that there will be no user
|
||||
// accounts left in Gophish with that role.
|
||||
var ErrModifyingOnlyAdmin = errors.New("Cannot remove the only administrator")
|
||||
|
||||
// User represents the user model for gophish.
|
||||
type User struct {
|
||||
Id int64 `json:"id"`
|
||||
|
@ -18,6 +29,13 @@ func GetUser(id int64) (User, error) {
|
|||
return u, err
|
||||
}
|
||||
|
||||
// GetUsers returns the users registered in Gophish
|
||||
func GetUsers() ([]User, error) {
|
||||
us := []User{}
|
||||
err := db.Preload("Role").Find(&us).Error
|
||||
return us, err
|
||||
}
|
||||
|
||||
// GetUserByAPIKey returns the user that the given API Key corresponds to. If no user is found, an
|
||||
// error is thrown.
|
||||
func GetUserByAPIKey(key string) (User, error) {
|
||||
|
@ -39,3 +57,102 @@ func PutUser(u *User) error {
|
|||
err := db.Save(u).Error
|
||||
return err
|
||||
}
|
||||
|
||||
// EnsureEnoughAdmins ensures that there is more than one user account in
|
||||
// Gophish with the Admin role. This function is meant to be called before
|
||||
// modifying a user account with the Admin role in a non-revokable way.
|
||||
func EnsureEnoughAdmins() error {
|
||||
role, err := GetRoleBySlug(RoleAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var adminCount int
|
||||
err = db.Model(&User{}).Where("role_id=?", role.ID).Count(&adminCount).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if adminCount == 1 {
|
||||
return ErrModifyingOnlyAdmin
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes the given user. To ensure that there is always at least
|
||||
// one user account with the Admin role, this function will refuse to delete
|
||||
// the last Admin.
|
||||
func DeleteUser(id int64) error {
|
||||
existing, err := GetUser(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If the user is an admin, we need to verify that it's not the last one.
|
||||
if existing.Role.Slug == RoleAdmin {
|
||||
err = EnsureEnoughAdmins()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
campaigns, err := GetCampaigns(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete the campaigns
|
||||
log.Infof("Deleting campaigns for user ID %d", id)
|
||||
for _, campaign := range campaigns {
|
||||
err = DeleteCampaign(campaign.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Infof("Deleting pages for user ID %d", id)
|
||||
// Delete the landing pages
|
||||
pages, err := GetPages(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, page := range pages {
|
||||
err = DeletePage(page.Id, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Delete the templates
|
||||
log.Infof("Deleting templates for user ID %d", id)
|
||||
templates, err := GetTemplates(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, template := range templates {
|
||||
err = DeleteTemplate(template.Id, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Delete the groups
|
||||
log.Infof("Deleting groups for user ID %d", id)
|
||||
groups, err := GetGroups(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range groups {
|
||||
err = DeleteGroup(&group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Delete the sending profiles
|
||||
log.Infof("Deleting sending profiles for user ID %d", id)
|
||||
profiles, err := GetSMTPs(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, profile := range profiles {
|
||||
err = DeleteSMTP(profile.Id, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Finally, delete the user
|
||||
err = db.Where("id=?", id).Delete(&User{}).Error
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -59,3 +59,44 @@ func (s *ModelsSuite) TestGeneratedAPIKey(c *check.C) {
|
|||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(u.ApiKey, check.Not(check.Equals), "12345678901234567890123456789012")
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) verifyRoleCount(c *check.C, roleID, expected int64) {
|
||||
var adminCount int64
|
||||
err := db.Model(&User{}).Where("role_id=?", roleID).Count(&adminCount).Error
|
||||
c.Assert(err, check.Equals, nil)
|
||||
c.Assert(adminCount, check.Equals, expected)
|
||||
}
|
||||
|
||||
func (s *ModelsSuite) TestDeleteLastAdmin(c *check.C) {
|
||||
// Create a new admin user
|
||||
role, err := GetRoleBySlug(RoleAdmin)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
newAdmin := User{
|
||||
Username: "new-admin",
|
||||
Hash: "123456",
|
||||
ApiKey: "123456",
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
}
|
||||
err = PutUser(&newAdmin)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
|
||||
// Ensure that there are two admins
|
||||
s.verifyRoleCount(c, role.ID, 2)
|
||||
|
||||
// Delete the newly created admin - this should work since we have more
|
||||
// than one current admin.
|
||||
err = DeleteUser(newAdmin.Id)
|
||||
c.Assert(err, check.Equals, nil)
|
||||
|
||||
// Verify that we now have one admin
|
||||
s.verifyRoleCount(c, role.ID, 1)
|
||||
|
||||
// Try to delete the last admin - this should fail since we always want at
|
||||
// least one admin active in Gophish.
|
||||
err = DeleteUser(1)
|
||||
c.Assert(err, check.Equals, ErrModifyingOnlyAdmin)
|
||||
|
||||
// Verify that the admin wasn't deleted
|
||||
s.verifyRoleCount(c, role.ID, 1)
|
||||
}
|
||||
|
|
27
package.json
27
package.json
|
@ -12,17 +12,22 @@
|
|||
},
|
||||
"homepage": "https://getgophish.com",
|
||||
"devDependencies": {
|
||||
"clean-css": "^3.4.23",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-clean-css": "^2.3.2",
|
||||
"gulp-cli": "^1.4.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-css": "^4.2.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-clean-css": "^4.0.0",
|
||||
"gulp-cli": "^2.2.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-jshint": "^2.0.4",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-util": "^3.0.8",
|
||||
"gulp-wrap": "^0.13.0",
|
||||
"jshint": "^2.9.4",
|
||||
"jshint-stylish": "^2.2.1"
|
||||
"gulp-jshint": "^2.1.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"gulp-wrap": "^0.15.0",
|
||||
"jshint": "^2.10.2",
|
||||
"jshint-stylish": "^2.2.1",
|
||||
"webpack": "^4.32.2",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,7 @@
|
|||
.swal2-popup {
|
||||
font-size: 1.6rem !important;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -702,6 +706,10 @@ table.dataTable {
|
|||
background-color: #37485a;
|
||||
}
|
||||
|
||||
.nav-badge {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#resultsMapContainer {
|
||||
display: none;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
var TEMPLATE_TAGS=[{id:1,name:"RId",description:"The unique ID for the recipient."},{id:2,name:"FirstName",description:"The recipient's first name."},{id:3,name:"LastName",description:"The recipient's last name."},{id:4,name:"Position",description:"The recipient's position."},{id:5,name:"From",description:"The address emails are sent from."},{id:6,name:"TrackingURL",description:"The URL to track emails being opened."},{id:7,name:"Tracker",description:"An HTML tag that adds a hidden tracking image (recommended instead of TrackingURL)."},{id:8,name:"URL",description:"The URL to your Gophish listener."},{id:9,name:"BaseURL",description:"The base URL with the path and rid parameter stripped. Useful for making links to static files."}],textTestCallback=function(e){return e.collapsed?CKEDITOR.plugins.textMatch.match(e,matchCallback):null},matchCallback=function(e,t){var i=/\{{2}\.?([A-z]|\})*$/,a=e.slice(0,t).match(i);return a?{start:a.index,end:t}:null},dataCallback=function(e,t){t(TEMPLATE_TAGS.filter(function(t){return 0==("{{."+t.name.toLowerCase()+"}}").indexOf(e.query.toLowerCase())}))},setupAutocomplete=function(e){e.on("instanceReady",function(e){new CKEDITOR.plugins.autocomplete(e.editor,{textTestCallback:textTestCallback,dataCallback:dataCallback,itemTemplate:'<li data-id="{id}"><div><strong class="item-title">{name}</strong></div><div><i>{description}</i></div></li>',outputTemplate:"[[.{name}]]"}).getHtmlToInsert=function(e){var t=this.outputTemplate.output(e);return t=t.replace("[[","{{").replace("]]","}}")}})};
|
||||
var TEMPLATE_TAGS=[{id:1,name:"RId",description:"The unique ID for the recipient."},{id:2,name:"FirstName",description:"The recipient's first name."},{id:3,name:"LastName",description:"The recipient's last name."},{id:4,name:"Position",description:"The recipient's position."},{id:5,name:"From",description:"The address emails are sent from."},{id:6,name:"TrackingURL",description:"The URL to track emails being opened."},{id:7,name:"Tracker",description:"An HTML tag that adds a hidden tracking image (recommended instead of TrackingURL)."},{id:8,name:"URL",description:"The URL to your Gophish listener."},{id:9,name:"BaseURL",description:"The base URL with the path and rid parameter stripped. Useful for making links to static files."}],textTestCallback=function(e){return e.collapsed?CKEDITOR.plugins.textMatch.match(e,matchCallback):null},matchCallback=function(e,t){var i=e.slice(0,t).match(/\{{2}\.?([A-z]|\})*$/);return i?{start:i.index,end:t}:null},dataCallback=function(t,e){e(TEMPLATE_TAGS.filter(function(e){return 0==("{{."+e.name.toLowerCase()+"}}").indexOf(t.query.toLowerCase())}))},setupAutocomplete=function(e){e.on("instanceReady",function(e){new CKEDITOR.plugins.autocomplete(e.editor,{textTestCallback:textTestCallback,dataCallback:dataCallback,itemTemplate:'<li data-id="{id}"><div><strong class="item-title">{name}</strong></div><div><i>{description}</i></div></li>',outputTemplate:"[[.{name}]]"}).getHtmlToInsert=function(e){var t=this.outputTemplate.output(e);return t=t.replace("[[","{{").replace("]]","}}")}})};
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function query(e,t,n,r){return $.ajax({url:"/api"+e+"?api_key="+user.api_key,async:r,method:t,data:JSON.stringify(n),dataType:"json",contentType:"application/json"})}function escapeHtml(e){return $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};$(document).ready(function(){var e=location.pathname;$(".nav-sidebar li").each(function(){var t=$(this);t.find("a").attr("href")===e&&t.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});
|
||||
function errorFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function successFlash(e){$("#flashes").empty(),$("#flashes").append('<div style="text-align:center" class="alert alert-success"> <i class="fa fa-check-circle"></i> '+e+"</div>")}function modalError(e){$("#modal\\.flashes").empty().append('<div style="text-align:center" class="alert alert-danger"> <i class="fa fa-exclamation-circle"></i> '+e+"</div>")}function query(e,t,r,n){return $.ajax({url:"/api"+e,async:n,method:t,data:JSON.stringify(r),dataType:"json",contentType:"application/json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)}})}function escapeHtml(e){return $("<div/>").text(e).html()}function unescapeHtml(e){return $("<div/>").html(e).text()}window.escapeHtml=escapeHtml;var capitalize=function(e){return e.charAt(0).toUpperCase()+e.slice(1)},api={campaigns:{get:function(){return query("/campaigns/","GET",{},!1)},post:function(e){return query("/campaigns/","POST",e,!1)},summary:function(){return query("/campaigns/summary","GET",{},!1)}},campaignId:{get:function(e){return query("/campaigns/"+e,"GET",{},!0)},delete:function(e){return query("/campaigns/"+e,"DELETE",{},!1)},results:function(e){return query("/campaigns/"+e+"/results","GET",{},!0)},complete:function(e){return query("/campaigns/"+e+"/complete","GET",{},!0)},summary:function(e){return query("/campaigns/"+e+"/summary","GET",{},!0)}},groups:{get:function(){return query("/groups/","GET",{},!1)},post:function(e){return query("/groups/","POST",e,!1)},summary:function(){return query("/groups/summary","GET",{},!0)}},groupId:{get:function(e){return query("/groups/"+e,"GET",{},!1)},put:function(e){return query("/groups/"+e.id,"PUT",e,!1)},delete:function(e){return query("/groups/"+e,"DELETE",{},!1)}},templates:{get:function(){return query("/templates/","GET",{},!1)},post:function(e){return query("/templates/","POST",e,!1)}},templateId:{get:function(e){return query("/templates/"+e,"GET",{},!1)},put:function(e){return query("/templates/"+e.id,"PUT",e,!1)},delete:function(e){return query("/templates/"+e,"DELETE",{},!1)}},pages:{get:function(){return query("/pages/","GET",{},!1)},post:function(e){return query("/pages/","POST",e,!1)}},pageId:{get:function(e){return query("/pages/"+e,"GET",{},!1)},put:function(e){return query("/pages/"+e.id,"PUT",e,!1)},delete:function(e){return query("/pages/"+e,"DELETE",{},!1)}},SMTP:{get:function(){return query("/smtp/","GET",{},!1)},post:function(e){return query("/smtp/","POST",e,!1)}},SMTPId:{get:function(e){return query("/smtp/"+e,"GET",{},!1)},put:function(e){return query("/smtp/"+e.id,"PUT",e,!1)},delete:function(e){return query("/smtp/"+e,"DELETE",{},!1)}},users:{get:function(){return query("/users/","GET",{},!0)},post:function(e){return query("/users/","POST",e,!0)}},userId:{get:function(e){return query("/users/"+e,"GET",{},!0)},put:function(e){return query("/users/"+e.id,"PUT",e,!0)},delete:function(e){return query("/users/"+e,"DELETE",{},!0)}},import_email:function(e){return query("/import/email","POST",e,!1)},clone_site:function(e){return query("/import/site","POST",e,!1)},send_test_email:function(e){return query("/util/send_test_email","POST",e,!0)},reset:function(){return query("/reset","POST",{},!0)}};window.api=api,$(document).ready(function(){var t=location.pathname;$(".nav-sidebar li").each(function(){var e=$(this);e.find("a").attr("href")===t&&e.addClass("active")}),$.fn.dataTable.moment("MMMM Do YYYY, h:mm:ss a"),$('[data-toggle="tooltip"]').tooltip()});
|
|
@ -0,0 +1 @@
|
|||
var groups=[];function save(e){var t=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,a){t.push({first_name:unescapeHtml(a[0]),last_name:unescapeHtml(a[1]),email:unescapeHtml(a[2]),position:unescapeHtml(a[3])})});var a={name:$("#name").val(),targets:t};-1!=e?(a.id=e,api.groupId.put(a).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(a).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group",dataType:"json",beforeSend:function(e){e.setRequestHeader("Authorization","Bearer "+user.api_key)},add:function(e,a){$("#modal\\.flashes").empty();var t=a.originalFiles[0].name;if(t&&!/(csv|txt)$/i.test(t.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}var downloadCSVTemplate=function(){var e="group_template.csv",a=Papa.unparse([{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,e);else{var o=window.URL.createObjectURL(t),s=document.createElement("a");s.href=o,s.setAttribute("download",e),document.body.appendChild(s),s.click(),document.body.removeChild(s)}},deleteGroup=function(o){var e=groups.find(function(e){return e.id===o});e&&Swal.fire({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(e.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(o).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(e){e.value&&Swal.fire("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};function addTarget(e,a,t,o){var s=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),s,escapeHtml(o),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(s);0<=i?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),0<e.total){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var t=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(groups,function(e,a){t.row.add([escapeHtml(a.name),escapeHtml(a.num_targets),moment(a.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+a.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+a.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}$(document).ready(function(){load(),$("#targetForm").submit(function(){var e=document.getElementById("targetForm");if(e.checkValidity())return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1;e.reportValidity()}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
function save(e){var a=[];$.each($("#targetsTable").DataTable().rows().data(),function(e,t){a.push({first_name:unescapeHtml(t[0]),last_name:unescapeHtml(t[1]),email:unescapeHtml(t[2]),position:unescapeHtml(t[3])})});var t={name:$("#name").val(),targets:a};-1!=e?(t.id=e,api.groupId.put(t).success(function(e){successFlash("Group updated successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.groups.post(t).success(function(e){successFlash("Group added successfully!"),load(),dismiss(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}function dismiss(){$("#targetsTable").dataTable().DataTable().clear().draw(),$("#name").val(""),$("#modal\\.flashes").empty()}function edit(e){if(targets=$("#targetsTable").dataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]}),$("#modalSubmit").unbind("click").click(function(){save(e)}),-1==e);else api.groupId.get(e).success(function(e){$("#name").val(e.name),$.each(e.targets,function(e,a){targets.DataTable().row.add([escapeHtml(a.first_name),escapeHtml(a.last_name),escapeHtml(a.email),escapeHtml(a.position),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>']).draw()})}).error(function(){errorFlash("Error fetching group")});$("#csvupload").fileupload({url:"/api/import/group?api_key="+user.api_key,dataType:"json",add:function(e,a){$("#modal\\.flashes").empty();var t=/(csv|txt)$/i,s=a.originalFiles[0].name;if(s&&!t.test(s.split(".").pop()))return modalError("Unsupported file extension (use .csv or .txt)"),!1;a.submit()},done:function(e,a){$.each(a.result,function(e,a){addTarget(a.first_name,a.last_name,a.email,a.position)}),targets.DataTable().draw()}})}function addTarget(e,a,t,s){var o=escapeHtml(t).toLowerCase(),r=[escapeHtml(e),escapeHtml(a),o,escapeHtml(s),'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'],n=targets.DataTable(),i=n.column(2,{order:"index"}).data().indexOf(o);i>=0?n.row(i,{order:"index"}).data(r):n.row.add(r)}function load(){$("#groupTable").hide(),$("#emptyMessage").hide(),$("#loading").show(),api.groups.summary().success(function(e){if($("#loading").hide(),e.total>0){groups=e.groups,$("#emptyMessage").hide(),$("#groupTable").show();var a=$("#groupTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});a.clear(),$.each(groups,function(e,t){a.row.add([escapeHtml(t.name),escapeHtml(t.num_targets),moment(t.modified_date).format("MMMM Do YYYY, h:mm:ss a"),"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit("+t.id+")'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger' onclick='deleteGroup("+t.id+")'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}else $("#emptyMessage").show()}).error(function(){errorFlash("Error fetching groups")})}var groups=[],downloadCSVTemplate=function(){var e=[{"First Name":"Example","Last Name":"User",Email:"foobar@example.com",Position:"Systems Administrator"}],a=Papa.unparse(e,{}),t=new Blob([a],{type:"text/csv;charset=utf-8;"});if(navigator.msSaveBlob)navigator.msSaveBlob(t,"group_template.csv");else{var s=window.URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.setAttribute("download","group_template.csv"),document.body.appendChild(o),o.click(),document.body.removeChild(o)}},deleteGroup=function(e){var a=groups.find(function(a){return a.id===e});a&&swal({title:"Are you sure?",text:"This will delete the group. This can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete "+escapeHtml(a.name),confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(a,t){api.groupId.delete(e).success(function(e){a()}).error(function(e){t(e.responseJSON.message)})})}}).then(function(){swal("Group Deleted!","This group has been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})};$(document).ready(function(){load(),$("#targetForm").submit(function(){return addTarget($("#firstName").val(),$("#lastName").val(),$("#email").val(),$("#position").val()),targets.DataTable().draw(),$("#targetForm>div>input").val(""),$("#firstName").focus(),!1}),$("#targetsTable").on("click","span>i.fa-trash-o",function(){targets.DataTable().row($(this).parents("tr")).remove().draw()}),$("#modal").on("hide.bs.modal",function(){dismiss()}),$("#csv-template").click(downloadCSVTemplate)});
|
||||
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){var r=[],n=function(){$("#username").val(""),$("#password").val(""),$("#confirm_password").val(""),$("#role").val(""),$("#modal\\.flashes").empty()},o=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){if($("#password").val()===$("#confirm_password").val()){var t={username:$("#username").val(),password:$("#password").val(),role:$("#role").val()};-1!=e?(t.id=e,api.userId.put(t).success(function(e){successFlash("User "+escapeHtml(t.username)+" updated successfully!"),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})):api.users.post(t).success(function(e){successFlash("User "+escapeHtml(t.username)+" registered successfully!"),s(),n(),$("#modal").modal("hide")}).error(function(e){modalError(e.responseJSON.message)})}else modalError("Passwords must match.")}(e)}),$("#role").select2(),-1==e?($("#role").val("user"),$("#role").trigger("change")):api.userId.get(e).success(function(e){$("#username").val(e.username),$("#role").val(e.role.slug),$("#role").trigger("change")}).error(function(){errorFlash("Error fetching user")})},s=function(){$("#userTable").hide(),$("#loading").show(),api.users.get().success(function(e){r=e,$("#loading").hide(),$("#userTable").show();var t=$("#userTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(r,function(e,r){t.row.add([escapeHtml(r.username),escapeHtml(r.role.name),"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='"+r.id+"'> <i class='fa fa-pencil'></i> </button> <button class='btn btn-danger delete_button' data-user-id='"+r.id+"'> <i class='fa fa-trash-o'></i> </button></div>"]).draw()})}).error(function(){errorFlash("Error fetching users")})};$(document).ready(function(){s(),$("#modal").on("hide.bs.modal",function(){n()}),$.fn.select2.defaults.set("width","100%"),$.fn.select2.defaults.set("dropdownParent",$("#role-select")),$.fn.select2.defaults.set("theme","bootstrap"),$.fn.select2.defaults.set("sorter",function(e){return e.sort(function(e,t){return e.text.toLowerCase()>t.text.toLowerCase()?1:e.text.toLowerCase()<t.text.toLowerCase()?-1:0})}),$("#new_button").on("click",function(){o(-1)}),$("#userTable").on("click",".edit_button",function(e){o($(this).attr("data-user-id"))}),$("#userTable").on("click",".delete_button",function(e){var t,n;t=$(this).attr("data-user-id"),(n=r.find(function(e){return e.id==t}))&&Swal.fire({title:"Are you sure?",text:"This will delete the account for "+escapeHtml(n.username)+" as well as all of the objects they have created.\n\nThis can't be undone!",type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,r){api.userId.delete(t).success(function(t){e()}).error(function(e){r(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("User Deleted!","The user account for "+escapeHtml(n.username)+" and all associated objects have been deleted!","success"),$('button:contains("OK")').on("click",function(){location.reload()})})})})}]);
|
File diff suppressed because one or more lines are too long
|
@ -125,7 +125,7 @@ function dismiss() {
|
|||
|
||||
// Deletes a campaign after prompting the user
|
||||
function deleteCampaign() {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the campaign. This can't be undone!",
|
||||
type: "warning",
|
||||
|
@ -147,12 +147,14 @@ function deleteCampaign() {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if(result.value){
|
||||
Swal.fire(
|
||||
'Campaign Deleted!',
|
||||
'This campaign has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.href = '/campaigns'
|
||||
})
|
||||
|
@ -161,7 +163,7 @@ function deleteCampaign() {
|
|||
|
||||
// Completes a campaign after prompting the user
|
||||
function completeCampaign() {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "Gophish will stop processing events for this campaign",
|
||||
type: "warning",
|
||||
|
@ -183,8 +185,9 @@ function completeCampaign() {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Campaign Completed!',
|
||||
'This campaign has been completed!',
|
||||
'success'
|
||||
|
@ -192,6 +195,7 @@ function completeCampaign() {
|
|||
$('#complete_button')[0].disabled = true;
|
||||
$('#complete_button').text('Completed!')
|
||||
doPoll = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -253,7 +257,7 @@ function replay(event_idx) {
|
|||
})
|
||||
/* Ensure we know where to send the user */
|
||||
// Prompt for the URL
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: 'Where do you want the credentials submitted to?',
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
|
@ -269,8 +273,10 @@ function replay(event_idx) {
|
|||
});
|
||||
}
|
||||
}).then(function (result) {
|
||||
url = result
|
||||
if (result.value){
|
||||
url = result.value
|
||||
submitForm()
|
||||
}
|
||||
})
|
||||
return
|
||||
submitForm()
|
||||
|
|
|
@ -13,7 +13,7 @@ var campaign = {}
|
|||
|
||||
// Launch attempts to POST to /campaigns/
|
||||
function launch() {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will schedule the campaign to be launched.",
|
||||
type: "question",
|
||||
|
@ -62,16 +62,18 @@ function launch() {
|
|||
.error(function (data) {
|
||||
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
||||
swal.close()
|
||||
Swal.close()
|
||||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Campaign Scheduled!',
|
||||
'This campaign has been scheduled for launch!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
window.location = "/campaigns/" + campaign.id.toString()
|
||||
})
|
||||
|
@ -124,7 +126,7 @@ function dismiss() {
|
|||
}
|
||||
|
||||
function deleteCampaign(idx) {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the campaign. This can't be undone!",
|
||||
type: "warning",
|
||||
|
@ -145,12 +147,14 @@ function deleteCampaign(idx) {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Campaign Deleted!',
|
||||
'This campaign has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
|
@ -166,8 +170,10 @@ function setupOptions() {
|
|||
} else {
|
||||
var group_s2 = $.map(groups, function (obj) {
|
||||
obj.text = obj.name
|
||||
obj.title = obj.targets.length + " targets"
|
||||
return obj
|
||||
});
|
||||
console.log(group_s2)
|
||||
$("#users.form-control").select2({
|
||||
placeholder: "Select Groups",
|
||||
data: group_s2,
|
||||
|
|
|
@ -17,18 +17,22 @@ function modalError(message) {
|
|||
|
||||
function query(endpoint, method, data, async) {
|
||||
return $.ajax({
|
||||
url: "/api" + endpoint + "?api_key=" + user.api_key,
|
||||
url: "/api" + endpoint,
|
||||
async: async,
|
||||
method: method,
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
contentType: "application/json"
|
||||
contentType: "application/json",
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return $("<div/>").text(text).html()
|
||||
}
|
||||
window.escapeHtml = escapeHtml
|
||||
|
||||
function unescapeHtml(html) {
|
||||
return $("<div/>").html(html).text()
|
||||
|
@ -193,6 +197,32 @@ var api = {
|
|||
return query("/smtp/" + id, "DELETE", {}, false)
|
||||
}
|
||||
},
|
||||
// users contains the endpoints for /users
|
||||
users: {
|
||||
// get() - Queries the API for GET /users
|
||||
get: function () {
|
||||
return query("/users/", "GET", {}, true)
|
||||
},
|
||||
// post() - Posts a user to POST /users
|
||||
post: function (user) {
|
||||
return query("/users/", "POST", user, true)
|
||||
}
|
||||
},
|
||||
// userId contains the endpoints for /users/:id
|
||||
userId: {
|
||||
// get() - Queries the API for GET /users/:id
|
||||
get: function (id) {
|
||||
return query("/users/" + id, "GET", {}, true)
|
||||
},
|
||||
// put() - Puts a user to PUT /users/:id
|
||||
put: function (user) {
|
||||
return query("/users/" + user.id, "PUT", user, true)
|
||||
},
|
||||
// delete() - Deletes a user at DELETE /users/:id
|
||||
delete: function (id) {
|
||||
return query("/users/" + id, "DELETE", {}, true)
|
||||
}
|
||||
},
|
||||
// import handles all of the "import" functions in the api
|
||||
import_email: function (req) {
|
||||
return query("/import/email", "POST", req, false)
|
||||
|
@ -209,6 +239,7 @@ var api = {
|
|||
return query("/reset", "POST", {}, true)
|
||||
}
|
||||
}
|
||||
window.api = api
|
||||
|
||||
// Register our moment.js datatables listeners
|
||||
$(document).ready(function () {
|
||||
|
|
|
@ -0,0 +1,292 @@
|
|||
var groups = []
|
||||
|
||||
// Save attempts to POST or PUT to /groups/
|
||||
function save(id) {
|
||||
var targets = []
|
||||
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
|
||||
targets.push({
|
||||
first_name: unescapeHtml(target[0]),
|
||||
last_name: unescapeHtml(target[1]),
|
||||
email: unescapeHtml(target[2]),
|
||||
position: unescapeHtml(target[3])
|
||||
})
|
||||
})
|
||||
var group = {
|
||||
name: $("#name").val(),
|
||||
targets: targets
|
||||
}
|
||||
// Submit the group
|
||||
if (id != -1) {
|
||||
// If we're just editing an existing group,
|
||||
// we need to PUT /groups/:id
|
||||
group.id = id
|
||||
api.groupId.put(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new group, POST it
|
||||
// to /groups
|
||||
api.groups.post(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group added successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
||||
$("#name").val("")
|
||||
$("#modal\\.flashes").empty()
|
||||
}
|
||||
|
||||
function edit(id) {
|
||||
targets = $("#targetsTable").dataTable({
|
||||
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
})
|
||||
$("#modalSubmit").unbind('click').click(function () {
|
||||
save(id)
|
||||
})
|
||||
if (id == -1) {
|
||||
var group = {}
|
||||
} else {
|
||||
api.groupId.get(id)
|
||||
.success(function (group) {
|
||||
$("#name").val(group.name)
|
||||
$.each(group.targets, function (i, record) {
|
||||
targets.DataTable()
|
||||
.row.add([
|
||||
escapeHtml(record.first_name),
|
||||
escapeHtml(record.last_name),
|
||||
escapeHtml(record.email),
|
||||
escapeHtml(record.position),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
]).draw()
|
||||
});
|
||||
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching group")
|
||||
})
|
||||
}
|
||||
// Handle file uploads
|
||||
$("#csvupload").fileupload({
|
||||
url: "/api/import/group",
|
||||
dataType: "json",
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + user.api_key);
|
||||
},
|
||||
add: function (e, data) {
|
||||
$("#modal\\.flashes").empty()
|
||||
var acceptFileTypes = /(csv|txt)$/i;
|
||||
var filename = data.originalFiles[0]['name']
|
||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||
modalError("Unsupported file extension (use .csv or .txt)")
|
||||
return false;
|
||||
}
|
||||
data.submit();
|
||||
},
|
||||
done: function (e, data) {
|
||||
$.each(data.result, function (i, record) {
|
||||
addTarget(
|
||||
record.first_name,
|
||||
record.last_name,
|
||||
record.email,
|
||||
record.position);
|
||||
});
|
||||
targets.DataTable().draw();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var downloadCSVTemplate = function () {
|
||||
var csvScope = [{
|
||||
'First Name': 'Example',
|
||||
'Last Name': 'User',
|
||||
'Email': 'foobar@example.com',
|
||||
'Position': 'Systems Administrator'
|
||||
}]
|
||||
var filename = 'group_template.csv'
|
||||
var csvString = Papa.unparse(csvScope, {})
|
||||
var csvData = new Blob([csvString], {
|
||||
type: 'text/csv;charset=utf-8;'
|
||||
});
|
||||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(csvData, filename);
|
||||
} else {
|
||||
var csvURL = window.URL.createObjectURL(csvData);
|
||||
var dlLink = document.createElement('a');
|
||||
dlLink.href = csvURL;
|
||||
dlLink.setAttribute('download', filename)
|
||||
document.body.appendChild(dlLink)
|
||||
dlLink.click();
|
||||
document.body.removeChild(dlLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var deleteGroup = function (id) {
|
||||
var group = groups.find(function (x) {
|
||||
return x.id === id
|
||||
})
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the group. This can't be undone!",
|
||||
type: "warning",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete " + escapeHtml(group.name),
|
||||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
preConfirm: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
api.groupId.delete(id)
|
||||
.success(function (msg) {
|
||||
resolve()
|
||||
})
|
||||
.error(function (data) {
|
||||
reject(data.responseJSON.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Group Deleted!',
|
||||
'This group has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
||||
// Create new data row.
|
||||
var email = escapeHtml(emailInput).toLowerCase();
|
||||
var newRow = [
|
||||
escapeHtml(firstNameInput),
|
||||
escapeHtml(lastNameInput),
|
||||
email,
|
||||
escapeHtml(positionInput),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
];
|
||||
|
||||
// Check table to see if email already exists.
|
||||
var targetsTable = targets.DataTable();
|
||||
var existingRowIndex = targetsTable
|
||||
.column(2, {
|
||||
order: "index"
|
||||
}) // Email column has index of 2
|
||||
.data()
|
||||
.indexOf(email);
|
||||
// Update or add new row as necessary.
|
||||
if (existingRowIndex >= 0) {
|
||||
targetsTable
|
||||
.row(existingRowIndex, {
|
||||
order: "index"
|
||||
})
|
||||
.data(newRow);
|
||||
} else {
|
||||
targetsTable.row.add(newRow);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
$("#groupTable").hide()
|
||||
$("#emptyMessage").hide()
|
||||
$("#loading").show()
|
||||
api.groups.summary()
|
||||
.success(function (response) {
|
||||
$("#loading").hide()
|
||||
if (response.total > 0) {
|
||||
groups = response.groups
|
||||
$("#emptyMessage").hide()
|
||||
$("#groupTable").show()
|
||||
var groupTable = $("#groupTable").DataTable({
|
||||
destroy: true,
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
});
|
||||
groupTable.clear();
|
||||
$.each(groups, function (i, group) {
|
||||
groupTable.row.add([
|
||||
escapeHtml(group.name),
|
||||
escapeHtml(group.num_targets),
|
||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
|
||||
<i class='fa fa-pencil'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
})
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching groups")
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
load()
|
||||
// Setup the event listeners
|
||||
// Handle manual additions
|
||||
$("#targetForm").submit(function () {
|
||||
// Validate the form data
|
||||
var targetForm = document.getElementById("targetForm")
|
||||
if (!targetForm.checkValidity()) {
|
||||
targetForm.reportValidity()
|
||||
return
|
||||
}
|
||||
addTarget(
|
||||
$("#firstName").val(),
|
||||
$("#lastName").val(),
|
||||
$("#email").val(),
|
||||
$("#position").val());
|
||||
targets.DataTable().draw();
|
||||
|
||||
// Reset user input.
|
||||
$("#targetForm>div>input").val('');
|
||||
$("#firstName").focus();
|
||||
return false;
|
||||
});
|
||||
// Handle Deletion
|
||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
||||
targets.DataTable()
|
||||
.row($(this).parents('tr'))
|
||||
.remove()
|
||||
.draw();
|
||||
});
|
||||
$("#modal").on("hide.bs.modal", function () {
|
||||
dismiss();
|
||||
});
|
||||
$("#csv-template").click(downloadCSVTemplate)
|
||||
});
|
|
@ -50,7 +50,7 @@ function dismiss() {
|
|||
}
|
||||
|
||||
var deletePage = function (idx) {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the landing page. This can't be undone!",
|
||||
type: "warning",
|
||||
|
@ -71,12 +71,14 @@ var deletePage = function (idx) {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Landing Page Deleted!',
|
||||
'This landing page has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
|
|
|
@ -107,7 +107,7 @@ var dismissSendTestEmailModal = function () {
|
|||
|
||||
|
||||
var deleteProfile = function (idx) {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the sending profile. This can't be undone!",
|
||||
type: "warning",
|
||||
|
@ -128,12 +128,14 @@ var deleteProfile = function (idx) {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'Sending Profile Deleted!',
|
||||
'This sending profile has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
|
|
|
@ -79,7 +79,7 @@ function dismiss() {
|
|||
}
|
||||
|
||||
var deleteTemplate = function (idx) {
|
||||
swal({
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the template. This can't be undone!",
|
||||
type: "warning",
|
||||
|
@ -100,12 +100,14 @@ var deleteTemplate = function (idx) {
|
|||
})
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
}).then(function (result) {
|
||||
if(result.value) {
|
||||
Swal.fire(
|
||||
'Template Deleted!',
|
||||
'This template has been deleted!',
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
|
|
|
@ -1,281 +1,183 @@
|
|||
var groups = []
|
||||
let users = []
|
||||
|
||||
// Save attempts to POST or PUT to /groups/
|
||||
function save(id) {
|
||||
var targets = []
|
||||
$.each($("#targetsTable").DataTable().rows().data(), function (i, target) {
|
||||
targets.push({
|
||||
first_name: unescapeHtml(target[0]),
|
||||
last_name: unescapeHtml(target[1]),
|
||||
email: unescapeHtml(target[2]),
|
||||
position: unescapeHtml(target[3])
|
||||
})
|
||||
})
|
||||
var group = {
|
||||
name: $("#name").val(),
|
||||
targets: targets
|
||||
}
|
||||
// Submit the group
|
||||
if (id != -1) {
|
||||
// If we're just editing an existing group,
|
||||
// we need to PUT /groups/:id
|
||||
group.id = id
|
||||
api.groupId.put(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new group, POST it
|
||||
// to /groups
|
||||
api.groups.post(group)
|
||||
.success(function (data) {
|
||||
successFlash("Group added successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
$("#targetsTable").dataTable().DataTable().clear().draw()
|
||||
$("#name").val("")
|
||||
$("#modal\\.flashes").empty()
|
||||
}
|
||||
|
||||
function edit(id) {
|
||||
targets = $("#targetsTable").dataTable({
|
||||
destroy: true, // Destroy any other instantiated table - http://datatables.net/manual/tech-notes/3#destroy
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
})
|
||||
$("#modalSubmit").unbind('click').click(function () {
|
||||
save(id)
|
||||
})
|
||||
if (id == -1) {
|
||||
var group = {}
|
||||
} else {
|
||||
api.groupId.get(id)
|
||||
.success(function (group) {
|
||||
$("#name").val(group.name)
|
||||
$.each(group.targets, function (i, record) {
|
||||
targets.DataTable()
|
||||
.row.add([
|
||||
escapeHtml(record.first_name),
|
||||
escapeHtml(record.last_name),
|
||||
escapeHtml(record.email),
|
||||
escapeHtml(record.position),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
]).draw()
|
||||
});
|
||||
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching group")
|
||||
})
|
||||
}
|
||||
// Handle file uploads
|
||||
$("#csvupload").fileupload({
|
||||
url: "/api/import/group?api_key=" + user.api_key,
|
||||
dataType: "json",
|
||||
add: function (e, data) {
|
||||
$("#modal\\.flashes").empty()
|
||||
var acceptFileTypes = /(csv|txt)$/i;
|
||||
var filename = data.originalFiles[0]['name']
|
||||
if (filename && !acceptFileTypes.test(filename.split(".").pop())) {
|
||||
modalError("Unsupported file extension (use .csv or .txt)")
|
||||
return false;
|
||||
}
|
||||
data.submit();
|
||||
},
|
||||
done: function (e, data) {
|
||||
$.each(data.result, function (i, record) {
|
||||
addTarget(
|
||||
record.first_name,
|
||||
record.last_name,
|
||||
record.email,
|
||||
record.position);
|
||||
});
|
||||
targets.DataTable().draw();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var downloadCSVTemplate = function () {
|
||||
var csvScope = [{
|
||||
'First Name': 'Example',
|
||||
'Last Name': 'User',
|
||||
'Email': 'foobar@example.com',
|
||||
'Position': 'Systems Administrator'
|
||||
}]
|
||||
var filename = 'group_template.csv'
|
||||
var csvString = Papa.unparse(csvScope, {})
|
||||
var csvData = new Blob([csvString], {
|
||||
type: 'text/csv;charset=utf-8;'
|
||||
});
|
||||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(csvData, filename);
|
||||
} else {
|
||||
var csvURL = window.URL.createObjectURL(csvData);
|
||||
var dlLink = document.createElement('a');
|
||||
dlLink.href = csvURL;
|
||||
dlLink.setAttribute('download', filename)
|
||||
document.body.appendChild(dlLink)
|
||||
dlLink.click();
|
||||
document.body.removeChild(dlLink)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var deleteGroup = function (id) {
|
||||
var group = groups.find(function (x) {
|
||||
return x.id === id
|
||||
})
|
||||
if (!group) {
|
||||
// Save attempts to POST or PUT to /users/
|
||||
const save = (id) => {
|
||||
// Validate that the passwords match
|
||||
if ($("#password").val() !== $("#confirm_password").val()) {
|
||||
modalError("Passwords must match.")
|
||||
return
|
||||
}
|
||||
swal({
|
||||
let user = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
role: $("#role").val()
|
||||
}
|
||||
// Submit the user
|
||||
if (id != -1) {
|
||||
// If we're just editing an existing user,
|
||||
// we need to PUT /user/:id
|
||||
user.id = id
|
||||
api.userId.put(user)
|
||||
.success(function (data) {
|
||||
successFlash("User " + escapeHtml(user.username) + " updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new user, POST it
|
||||
// to /user
|
||||
api.users.post(user)
|
||||
.success(function (data) {
|
||||
successFlash("User " + escapeHtml(user.username) + " registered successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dismiss = () => {
|
||||
$("#username").val("")
|
||||
$("#password").val("")
|
||||
$("#confirm_password").val("")
|
||||
$("#role").val("")
|
||||
$("#modal\\.flashes").empty()
|
||||
}
|
||||
|
||||
const edit = (id) => {
|
||||
$("#modalSubmit").unbind('click').click(() => {
|
||||
save(id)
|
||||
})
|
||||
$("#role").select2()
|
||||
if (id == -1) {
|
||||
$("#role").val("user")
|
||||
$("#role").trigger("change")
|
||||
} else {
|
||||
api.userId.get(id)
|
||||
.success(function (user) {
|
||||
$("#username").val(user.username)
|
||||
$("#role").val(user.role.slug)
|
||||
$("#role").trigger("change")
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching user")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = (id) => {
|
||||
var user = users.find(x => x.id == id)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
Swal.fire({
|
||||
title: "Are you sure?",
|
||||
text: "This will delete the group. This can't be undone!",
|
||||
text: "This will delete the account for " + escapeHtml(user.username) + " as well as all of the objects they have created.\n\nThis can't be undone!",
|
||||
type: "warning",
|
||||
animation: false,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete " + escapeHtml(group.name),
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#428bca",
|
||||
reverseButtons: true,
|
||||
allowOutsideClick: false,
|
||||
preConfirm: function () {
|
||||
return new Promise(function (resolve, reject) {
|
||||
api.groupId.delete(id)
|
||||
.success(function (msg) {
|
||||
return new Promise((resolve, reject) => {
|
||||
api.userId.delete(id)
|
||||
.success((msg) => {
|
||||
resolve()
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
reject(data.responseJSON.message)
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
Swal.showValidationMessage(error)
|
||||
})
|
||||
}
|
||||
}).then(function () {
|
||||
swal(
|
||||
'Group Deleted!',
|
||||
'This group has been deleted!',
|
||||
}).then(function (result) {
|
||||
if (result.value){
|
||||
Swal.fire(
|
||||
'User Deleted!',
|
||||
"The user account for " + escapeHtml(user.username) + " and all associated objects have been deleted!",
|
||||
'success'
|
||||
);
|
||||
}
|
||||
$('button:contains("OK")').on('click', function () {
|
||||
location.reload()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addTarget(firstNameInput, lastNameInput, emailInput, positionInput) {
|
||||
// Create new data row.
|
||||
var email = escapeHtml(emailInput).toLowerCase();
|
||||
var newRow = [
|
||||
escapeHtml(firstNameInput),
|
||||
escapeHtml(lastNameInput),
|
||||
email,
|
||||
escapeHtml(positionInput),
|
||||
'<span style="cursor:pointer;"><i class="fa fa-trash-o"></i></span>'
|
||||
];
|
||||
|
||||
// Check table to see if email already exists.
|
||||
var targetsTable = targets.DataTable();
|
||||
var existingRowIndex = targetsTable
|
||||
.column(2, {
|
||||
order: "index"
|
||||
}) // Email column has index of 2
|
||||
.data()
|
||||
.indexOf(email);
|
||||
// Update or add new row as necessary.
|
||||
if (existingRowIndex >= 0) {
|
||||
targetsTable
|
||||
.row(existingRowIndex, {
|
||||
order: "index"
|
||||
})
|
||||
.data(newRow);
|
||||
} else {
|
||||
targetsTable.row.add(newRow);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
$("#groupTable").hide()
|
||||
$("#emptyMessage").hide()
|
||||
const load = () => {
|
||||
$("#userTable").hide()
|
||||
$("#loading").show()
|
||||
api.groups.summary()
|
||||
.success(function (response) {
|
||||
api.users.get()
|
||||
.success((us) => {
|
||||
users = us
|
||||
$("#loading").hide()
|
||||
if (response.total > 0) {
|
||||
groups = response.groups
|
||||
$("#emptyMessage").hide()
|
||||
$("#groupTable").show()
|
||||
var groupTable = $("#groupTable").DataTable({
|
||||
$("#userTable").show()
|
||||
let userTable = $("#userTable").DataTable({
|
||||
destroy: true,
|
||||
columnDefs: [{
|
||||
orderable: false,
|
||||
targets: "no-sort"
|
||||
}]
|
||||
});
|
||||
groupTable.clear();
|
||||
$.each(groups, function (i, group) {
|
||||
groupTable.row.add([
|
||||
escapeHtml(group.name),
|
||||
escapeHtml(group.num_targets),
|
||||
moment(group.modified_date).format('MMMM Do YYYY, h:mm:ss a'),
|
||||
"<div class='pull-right'><button class='btn btn-primary' data-toggle='modal' data-backdrop='static' data-target='#modal' onclick='edit(" + group.id + ")'>\
|
||||
userTable.clear();
|
||||
$.each(users, (i, user) => {
|
||||
userTable.row.add([
|
||||
escapeHtml(user.username),
|
||||
escapeHtml(user.role.name),
|
||||
"<div class='pull-right'><button class='btn btn-primary edit_button' data-toggle='modal' data-backdrop='static' data-target='#modal' data-user-id='" + user.id + "'>\
|
||||
<i class='fa fa-pencil'></i>\
|
||||
</button>\
|
||||
<button class='btn btn-danger' onclick='deleteGroup(" + group.id + ")'>\
|
||||
<button class='btn btn-danger delete_button' data-user-id='" + user.id + "'>\
|
||||
<i class='fa fa-trash-o'></i>\
|
||||
</button></div>"
|
||||
]).draw()
|
||||
})
|
||||
} else {
|
||||
$("#emptyMessage").show()
|
||||
}
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching groups")
|
||||
.error(() => {
|
||||
errorFlash("Error fetching users")
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
load()
|
||||
// Setup the event listeners
|
||||
// Handle manual additions
|
||||
$("#targetForm").submit(function () {
|
||||
addTarget(
|
||||
$("#firstName").val(),
|
||||
$("#lastName").val(),
|
||||
$("#email").val(),
|
||||
$("#position").val());
|
||||
targets.DataTable().draw();
|
||||
|
||||
// Reset user input.
|
||||
$("#targetForm>div>input").val('');
|
||||
$("#firstName").focus();
|
||||
return false;
|
||||
});
|
||||
// Handle Deletion
|
||||
$("#targetsTable").on("click", "span>i.fa-trash-o", function () {
|
||||
targets.DataTable()
|
||||
.row($(this).parents('tr'))
|
||||
.remove()
|
||||
.draw();
|
||||
});
|
||||
$("#modal").on("hide.bs.modal", function () {
|
||||
dismiss();
|
||||
});
|
||||
$("#csv-template").click(downloadCSVTemplate)
|
||||
// Select2 Defaults
|
||||
$.fn.select2.defaults.set("width", "100%");
|
||||
$.fn.select2.defaults.set("dropdownParent", $("#role-select"));
|
||||
$.fn.select2.defaults.set("theme", "bootstrap");
|
||||
$.fn.select2.defaults.set("sorter", function (data) {
|
||||
return data.sort(function (a, b) {
|
||||
if (a.text.toLowerCase() > b.text.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
if (a.text.toLowerCase() < b.text.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
})
|
||||
$("#new_button").on("click", function () {
|
||||
edit(-1)
|
||||
})
|
||||
$("#userTable").on('click', '.edit_button', function (e) {
|
||||
edit($(this).attr('data-user-id'))
|
||||
})
|
||||
$("#userTable").on('click', '.delete_button', function (e) {
|
||||
deleteUser($(this).attr('data-user-id'))
|
||||
})
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -43,20 +43,6 @@
|
|||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/">Dashboard</a>
|
||||
</li>
|
||||
<li><a href="/campaigns">Campaigns</a>
|
||||
</li>
|
||||
<li><a href="/users">Users & Groups</a>
|
||||
</li>
|
||||
<li><a href="/templates">Email Templates</a>
|
||||
</li>
|
||||
<li><a href="/landing_pages">Landing Pages</a>
|
||||
</li>
|
||||
<li><a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li><a href="/settings">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
{{if .User}}
|
||||
<div class="btn-group" id="navbar-dropdown">
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
{{define "body"}}
|
||||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
<div class="row">
|
||||
<h1 class="page-header">
|
||||
Users & Groups
|
||||
</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<div class="row">
|
||||
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
|
||||
data-target="#modal">
|
||||
<i class="fa fa-plus"></i> New Group</button>
|
||||
</div>
|
||||
|
||||
<div id="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||
</div>
|
||||
<div id="emptyMessage" class="row" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
No groups created yet. Let's create one!
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table id="groupTable" class="table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th># of Members</th>
|
||||
<th>Modified Date</th>
|
||||
<th class="col-md-2 no-sort"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="modal" tabindex="-1" role="dialog" aria-labelledby="modalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row" id="modal.flashes"></div>
|
||||
<label class="control-label" for="name">Name:</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
||||
autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right"
|
||||
title="Supports CSV files" id="fileUpload">
|
||||
<i class="fa fa-plus"></i> Bulk Import Users
|
||||
<input type="file" id="csvupload" multiple>
|
||||
</span>
|
||||
<span id="csv-template" class="text-muted small">
|
||||
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<form id="targetForm">
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" placeholder="First Name" id="firstName">
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="email" class="form-control" placeholder="Email" id="email" required>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" placeholder="Position" id="position">
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button type="submit" class="btn btn-danger btn-lg">
|
||||
<i class="fa fa-plus"></i> Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<br />
|
||||
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th>Email</th>
|
||||
<th>Position</th>
|
||||
<th class="no-sort"></th>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="modalSubmit">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/groups.min.js"></script>
|
||||
{{end}}
|
|
@ -10,7 +10,7 @@
|
|||
<a href="/campaigns">Campaigns</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/users">Users & Groups</a>
|
||||
<a href="/groups">Users & Groups</a>
|
||||
</li>
|
||||
<li> <a href="/templates">Email Templates</a>
|
||||
</li>
|
||||
|
@ -21,8 +21,13 @@
|
|||
<a href="/sending_profiles">Sending Profiles</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings">Settings <span class="badge pull-right">Admin</span></a>
|
||||
<a href="/settings">Account Settings</span></a>
|
||||
</li>
|
||||
{{if .ModifySystem}}
|
||||
<li>
|
||||
<a href="/users">User Management<span class="nav-badge badge pull-right">Admin</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
<li>
|
||||
<hr>
|
||||
</li>
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
|
||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
||||
<link rel="shortcut icon" href="/favicon.png">
|
||||
|
||||
<title>Gophish - {{ .Title }}</title>
|
||||
|
||||
<link href="/css/dist/gophish.css" rel='stylesheet' type='text/css'>
|
||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<img class="navbar-logo" src="/images/logo_inv_small.png" />
|
||||
<a class="navbar-brand" href="/"> gophish</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a id="login-button" href="/login">
|
||||
<button type="button" class="btn btn-primary">Login</button>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<form class="form-signin" action="/register" method="POST">
|
||||
<img id="logo" src="/images/logo_purple.png" />
|
||||
<h2 class="form-signin-heading">Please register below</h2>
|
||||
{{template "flashes" .Flashes}}
|
||||
<input type="text" name="username" class="form-control top-input" placeholder="Username" required autofocus />
|
||||
<input type="password" name="password" class="form-control middle-input" placeholder="Password"
|
||||
autocomplete="off" required />
|
||||
<input type="password" name="confirm_password" class="form-control bottom-input" placeholder="Confirm Password"
|
||||
autocomplete="off" required />
|
||||
<input type="hidden" name="csrf_token" value="{{.Token}}" />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="/js/dist/vendor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
|
@ -8,7 +8,8 @@
|
|||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
||||
data-toggle="tab">Account Settings</a></li>
|
||||
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI Settings</a></li>
|
||||
<li role="uiSettings"><a href="#uiSettings" aria-controls="uiSettings" role="tab" data-toggle="tab">UI
|
||||
Settings</a></li>
|
||||
</ul>
|
||||
<!-- Tab Panes -->
|
||||
<div class="tab-content">
|
||||
|
@ -22,19 +23,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label class="col-sm-2 control-label form-label">Register a New User</label>
|
||||
<div class="col-md-6">
|
||||
<a href="/register" class="btn btn-primary"><i class="fa fa-plus"></i> Add User</a>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<label for="api_key" class="col-sm-2 control-label form-label">API Key:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}" class="form-control"
|
||||
readonly />
|
||||
<input type="text" id="api_key" onclick="this.select();" value="{{.User.ApiKey}}"
|
||||
class="form-control" readonly />
|
||||
</div>
|
||||
<form id="apiResetForm">
|
||||
<button class="btn btn-primary"><i class="fa fa-refresh" type="submit"></i> Reset</button>
|
||||
|
@ -46,26 +40,30 @@
|
|||
<div class="row">
|
||||
<label for="username" class="col-sm-2 control-label form-label">Username:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="text" id="username" name="username" value="{{.User.Username}}" class="form-control" />
|
||||
<input type="text" id="username" name="username" value="{{.User.Username}}"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label for="current_password" class="col-sm-2 control-label form-label">Old Password:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" id="current_password" name="current_password" autocomplete="off" class="form-control" />
|
||||
<input type="password" id="current_password" name="current_password" autocomplete="off"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" id="new_password" name="new_password" autocomplete="off" class="form-control" />
|
||||
<input type="password" id="new_password" name="new_password" autocomplete="off"
|
||||
class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="row">
|
||||
<label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New Password:</label>
|
||||
<label for="confirm_new_password" class="col-sm-2 control-label form-label">Confirm New
|
||||
Password:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" id="confirm_new_password" name="confirm_new_password" autocomplete="off"
|
||||
class="form-control" />
|
||||
|
|
|
@ -2,31 +2,25 @@
|
|||
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
|
||||
<div class="row">
|
||||
<h1 class="page-header">
|
||||
Users & Groups
|
||||
{{.Title}}
|
||||
</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<div class="row">
|
||||
<button type="button" class="btn btn-primary" onclick="edit(-1)" data-toggle="modal" data-backdrop="static"
|
||||
data-target="#modal">
|
||||
<i class="fa fa-plus"></i> New Group</button>
|
||||
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||
data-user-id="-1" data-target="#modal">
|
||||
<i class="fa fa-plus"></i> New User</button>
|
||||
</div>
|
||||
|
||||
<div id="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||
</div>
|
||||
<div id="emptyMessage" class="row" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
No groups created yet. Let's create one!
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table id="groupTable" class="table" style="display:none;">
|
||||
<table id="userTable" class="table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th># of Members</th>
|
||||
<th>Modified Date</th>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th class="col-md-2 no-sort"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -43,56 +37,30 @@
|
|||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="groupModalLabel">New Group</h4>
|
||||
<h4 class="modal-title" id="groupModalLabel">New User</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body" id="modal_body">
|
||||
<div class="row" id="modal.flashes"></div>
|
||||
<label class="control-label" for="name">Name:</label>
|
||||
<label class="control-label" for="username">Username:</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" ng-model="group.name" placeholder="Group name" id="name"
|
||||
autofocus />
|
||||
<input type="text" class="form-control" placeholder="Username" id="username" autofocus />
|
||||
</div>
|
||||
<label class="control-label" for="password">Password:</label>
|
||||
<div class="form-group">
|
||||
<span class="btn btn-danger btn-file" data-toggle="tooltip" data-placement="right" title="Supports CSV files"
|
||||
id="fileUpload">
|
||||
<i class="fa fa-plus"></i> Bulk Import Users
|
||||
<input type="file" id="csvupload" multiple>
|
||||
</span>
|
||||
<span id="csv-template" class="text-muted small">
|
||||
<i class="fa fa-file-excel-o"></i> Download CSV Template</span>
|
||||
<input type="password" class="form-control" placeholder="Password" id="password" required />
|
||||
</div>
|
||||
<div class="row">
|
||||
<form id="targetForm">
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" placeholder="First Name" id="firstName">
|
||||
<label class="control-label" for="confirm_password">Confirm Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
|
||||
required />
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<input type="text" class="form-control" placeholder="Last Name" id="lastName">
|
||||
<label class="control-label" for="role">Role:</label>
|
||||
<div class="form-group" id="role-select">
|
||||
<select class="form-control" placeholder="" id="role" />
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="email" class="form-control" placeholder="Email" id="email" required>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" placeholder="Position" id="position">
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button type="submit" class="btn btn-danger btn-lg">
|
||||
<i class="fa fa-plus"></i> Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<br />
|
||||
<table id="targetsTable" class="table table-hover table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
<th>Email</th>
|
||||
<th>Position</th>
|
||||
<th class="no-sort"></th>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
|
22
util/util.go
22
util/util.go
|
@ -21,6 +21,7 @@ import (
|
|||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jordan-wright/email"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -86,6 +87,9 @@ func ParseCSV(r *http.Request) ([]models.Target, error) {
|
|||
pi = i
|
||||
}
|
||||
}
|
||||
if fi == -1 && li == -1 && ei == -1 && pi == -1 {
|
||||
continue
|
||||
}
|
||||
for {
|
||||
record, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
|
@ -187,3 +191,21 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
log.Info("TLS Certificate Generation complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSecureKey creates a secure key to use as an API key
|
||||
func GenerateSecureKey() string {
|
||||
// Inspired from gorilla/securecookie
|
||||
k := make([]byte, 32)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// NewHash hashes the provided password and returns the bcrypt hash (using the
|
||||
// default 10 rounds) as a string.
|
||||
func NewHash(pass string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
|
||||
entry: {
|
||||
users: './users',
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'static', 'js', 'dist', 'app'),
|
||||
filename: '[name].min.js'
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue