Merge branch 'master' into feature/spoofed-hostname

pull/1400/head
Russel Van Tuyl 2019-12-02 10:11:12 -05:00 committed by GitHub
commit 905e02bdb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 7981 additions and 1488 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@ -5,6 +5,8 @@ go:
- 1.9
- "1.10"
- 1.11
- 1.12
- 1.13
- tip
install:

45
Dockerfile Normal file
View File

@ -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"]

View File

@ -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 {

View File

@ -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"`

View File

@ -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"`
}

133
controllers/api/api_test.go Normal file
View File

@ -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))
}

137
controllers/api/campaign.go Normal file
View File

@ -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)
}
}

118
controllers/api/group.go Normal file
View File

@ -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)
}
}

157
controllers/api/import.go Normal file
View File

@ -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
}

91
controllers/api/page.go Normal file
View File

@ -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)
}
}

24
controllers/api/reset.go Normal file
View File

@ -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)
}
}
}

View File

@ -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)
}

79
controllers/api/server.go Normal file
View File

@ -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)
}

96
controllers/api/smtp.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

218
controllers/api/user.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}

122
controllers/api/util.go Normal file
View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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) {

74
docker/run.sh Executable file
View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 {
q := r.URL.Query()
q.Set("next", r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
return
}
q := r.URL.Query()
q.Set("next", r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/login?%s", q.Encode()), http.StatusTemporaryRedirect)
return
}
}

View File

@ -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))
}

23
middleware/session.go Normal file
View File

@ -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)))

View File

@ -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
}

View File

@ -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
db, err = gorm.Open(conf.DBName, conf.DBPath)
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)

View File

@ -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"`

View File

@ -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

View File

@ -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 = ""

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

8
static/css/main.css vendored
View File

@ -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

View File

@ -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

View File

@ -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()});

1
static/js/dist/app/groups.min.js vendored Normal file
View File

@ -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

View File

@ -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()})})})})}]);

31
static/js/dist/vendor.min.js vendored Executable file → Normal file

File diff suppressed because one or more lines are too long

View File

@ -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(
'Campaign Deleted!',
'This campaign has been deleted!',
'success'
);
}).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,15 +185,17 @@ function completeCampaign() {
})
})
}
}).then(function () {
swal(
'Campaign Completed!',
'This campaign has been completed!',
'success'
);
$('#complete_button')[0].disabled = true;
$('#complete_button').text('Completed!')
doPoll = false;
}).then(function (result) {
if (result.value){
Swal.fire(
'Campaign Completed!',
'This campaign has been completed!',
'success'
);
$('#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
submitForm()
if (result.value){
url = result.value
submitForm()
}
})
return
submitForm()

View File

@ -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(
'Campaign Scheduled!',
'This campaign has been scheduled for launch!',
'success'
);
}).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(
'Campaign Deleted!',
'This campaign has been deleted!',
'success'
);
}).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,

View File

@ -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 () {

292
static/js/src/app/groups.js Normal file
View File

@ -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)
});

View File

@ -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(
'Landing Page Deleted!',
'This landing page has been deleted!',
'success'
);
}).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()
})

View File

@ -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(
'Sending Profile Deleted!',
'This sending profile has been deleted!',
'success'
);
}).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()
})

View File

@ -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(
'Template Deleted!',
'This template has been deleted!',
'success'
);
}).then(function (result) {
if(result.value) {
Swal.fire(
'Template Deleted!',
'This template has been deleted!',
'success'
);
}
$('button:contains("OK")').on('click', function () {
location.reload()
})

View File

@ -1,28 +1,25 @@
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
// 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
}
// Submit the group
let user = {
username: $("#username").val(),
password: $("#password").val(),
role: $("#role").val()
}
// Submit the user
if (id != -1) {
// If we're just editing an existing group,
// we need to PUT /groups/:id
group.id = id
api.groupId.put(group)
// 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("Group updated successfully!")
successFlash("User " + escapeHtml(user.username) + " updated successfully!")
load()
dismiss()
$("#modal").modal('hide')
@ -31,11 +28,11 @@ function save(id) {
modalError(data.responseJSON.message)
})
} else {
// Else, if this is a new group, POST it
// to /groups
api.groups.post(group)
// Else, if this is a new user, POST it
// to /user
api.users.post(user)
.success(function (data) {
successFlash("Group added successfully!")
successFlash("User " + escapeHtml(user.username) + " registered successfully!")
load()
dismiss()
$("#modal").modal('hide')
@ -46,236 +43,141 @@ function save(id) {
}
}
function dismiss() {
$("#targetsTable").dataTable().DataTable().clear().draw()
$("#name").val("")
const dismiss = () => {
$("#username").val("")
$("#password").val("")
$("#confirm_password").val("")
$("#role").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 () {
const edit = (id) => {
$("#modalSubmit").unbind('click').click(() => {
save(id)
})
$("#role").select2()
if (id == -1) {
var group = {}
$("#role").val("user")
$("#role").trigger("change")
} 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()
});
api.userId.get(id)
.success(function (user) {
$("#username").val(user.username)
$("#role").val(user.role.slug)
$("#role").trigger("change")
})
.error(function () {
errorFlash("Error fetching group")
errorFlash("Error fetching user")
})
}
// 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) {
const deleteUser = (id) => {
var user = users.find(x => x.id == id)
if (!user) {
return
}
swal({
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 (result) {
if (result.value){
Swal.fire(
'User Deleted!',
"The user account for " + escapeHtml(user.username) + " and all associated objects have been deleted!",
'success'
);
}
}).then(function () {
swal(
'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()
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({
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").show()
let userTable = $("#userTable").DataTable({
destroy: true,
columnDefs: [{
orderable: false,
targets: "no-sort"
}]
});
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()
}
]).draw()
})
})
.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

View File

@ -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 &amp; 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">
@ -84,4 +70,4 @@
</body>
</html>
{{ end }}
{{ end }}

106
templates/groups.html Normal file
View File

@ -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 &amp; 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>
&nbsp;
<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">&times;</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}}

View File

@ -10,7 +10,7 @@
<a href="/campaigns">Campaigns</a>
</li>
<li>
<a href="/users">Users &amp; Groups</a>
<a href="/groups">Users &amp; 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>

View File

@ -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="/">&nbsp;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 }}

View File

@ -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" />

View File

@ -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 &amp; 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>
&nbsp;
<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">&times;</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">
</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>
<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>
<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>
<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>

View File

@ -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
}

21
webpack.config.js Normal file
View File

@ -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"
}
}]
}
}

5244
yarn.lock Normal file

File diff suppressed because it is too large Load Diff