Browse Source

Refactor servers (#1321)

* Refactoring servers to support custom workers and graceful shutdown.
* Refactoring workers to support custom mailers.
* Refactoring mailer to be an interface, with proper instances instead of a single global instance
* Cleaning up a few things. Locking maillogs for campaigns set to launch immediately to prevent a race condition.
* Cleaning up API middleware to be simpler
* Moving template parameters to separate struct
* Changed LoadConfig to return config object
* Cleaned up some error handling, removing uninitialized global error in models package
* Changed static file serving to use the unindexed package
pull/1323/head
Jordan Wright 2 years ago
committed by GitHub
parent
commit
47f0049c30
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 551 additions and 517 deletions
  1. +8
    -10
      config/config.go
  2. +4
    -4
      config/config_test.go
  3. +23
    -32
      controllers/api.go
  4. +25
    -24
      controllers/api_test.go
  5. +97
    -21
      controllers/phish.go
  6. +12
    -12
      controllers/phish_test.go
  7. +166
    -133
      controllers/route.go
  8. +7
    -7
      controllers/route_test.go
  9. +0
    -35
      controllers/static.go
  10. +0
    -81
      controllers/static_test.go
  11. +28
    -53
      gophish.go
  12. +2
    -2
      logger/logger.go
  13. +15
    -12
      mailer/mailer.go
  14. +4
    -4
      mailer/mailer_test.go
  15. +8
    -6
      middleware/middleware.go
  16. +16
    -6
      models/campaign.go
  17. +26
    -0
      models/campaign_test.go
  18. +2
    -2
      models/email_request.go
  19. +8
    -8
      models/email_request_test.go
  20. +5
    -5
      models/group.go
  21. +4
    -6
      models/maillog.go
  22. +9
    -9
      models/maillog_test.go
  23. +7
    -5
      models/models.go
  24. +10
    -5
      models/models_test.go
  25. +1
    -1
      models/page.go
  26. +1
    -1
      models/smtp.go
  27. +4
    -4
      models/smtp_test.go
  28. +5
    -5
      models/template.go
  29. +6
    -4
      util/util_test.go
  30. +39
    -10
      worker/worker.go
  31. +9
    -10
      worker/worker_test.go

+ 8
- 10
config/config.go View File

@ -38,9 +38,6 @@ type Config struct {
Logging LoggingConfig `json:"logging"`
}
// Conf contains the initialized configuration struct
var Conf Config
// Version contains the current gophish version
var Version = ""
@ -48,19 +45,20 @@ var Version = ""
const ServerName = "gophish"
// LoadConfig loads the configuration from the specified filepath
func LoadConfig(filepath string) error {
func LoadConfig(filepath string) (*Config, error) {
// Get the config file
configFile, err := ioutil.ReadFile(filepath)
if err != nil {
return err
return nil, err
}
err = json.Unmarshal(configFile, &Conf)
config := &Config{}
err = json.Unmarshal(configFile, config)
if err != nil {
return err
return nil, err
}
// Choosing the migrations directory based on the database used.
Conf.MigrationsPath = Conf.MigrationsPath + Conf.DBName
config.MigrationsPath = config.MigrationsPath + config.DBName
// Explicitly set the TestFlag to false to prevent config.json overrides
Conf.TestFlag = false
return nil
config.TestFlag = false
return config, nil
}

+ 4
- 4
config/config_test.go View File

@ -48,18 +48,18 @@ func (s *ConfigSuite) TestLoadConfig() {
_, err := s.ConfigFile.Write(validConfig)
s.Nil(err)
// Load the valid config
err = LoadConfig(s.ConfigFile.Name())
conf, err := LoadConfig(s.ConfigFile.Name())
s.Nil(err)
expectedConfig := Config{}
expectedConfig := &Config{}
err = json.Unmarshal(validConfig, &expectedConfig)
s.Nil(err)
expectedConfig.MigrationsPath = expectedConfig.MigrationsPath + expectedConfig.DBName
expectedConfig.TestFlag = false
s.Equal(expectedConfig, Conf)
s.Equal(expectedConfig, conf)
// Load an invalid config
err = LoadConfig("bogusfile")
conf, err = LoadConfig("bogusfile")
s.NotNil(err)
}

+ 23
- 32
controllers/api.go View File

@ -17,23 +17,14 @@ import (
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gophish/gophish/worker"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
"github.com/jordan-wright/email"
"github.com/sirupsen/logrus"
)
// Worker is the worker that processes phishing events and updates campaigns.
var Worker *worker.Worker
func init() {
Worker = worker.New()
go Worker.Start()
}
// API (/api/reset) resets a user's API key
func API_Reset(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Reset(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "POST":
u := ctx.Get(r, "user").(models.User)
@ -49,7 +40,7 @@ func API_Reset(w http.ResponseWriter, r *http.Request) {
// API_Campaigns returns a list of campaigns if requested via GET.
// If requested via POST, API_Campaigns creates a new campaign and returns a reference to it.
func API_Campaigns(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaigns(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
cs, err := models.GetCampaigns(ctx.Get(r, "user_id").(int64))
@ -74,14 +65,14 @@ func API_Campaigns(w http.ResponseWriter, r *http.Request) {
// 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.CAMPAIGN_IN_PROGRESS {
go Worker.LaunchCampaign(c)
go as.worker.LaunchCampaign(c)
}
JSONResponse(w, c, http.StatusCreated)
}
}
// API_Campaigns_Summary returns the summary for the current user's campaigns
func API_Campaigns_Summary(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaigns_Summary(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
cs, err := models.GetCampaignSummaries(ctx.Get(r, "user_id").(int64))
@ -96,7 +87,7 @@ func API_Campaigns_Summary(w http.ResponseWriter, r *http.Request) {
// API_Campaigns_Id returns details about the requested campaign. If the campaign is not
// valid, API_Campaigns_Id returns null.
func API_Campaigns_Id(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaigns_Id(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))
@ -120,7 +111,7 @@ func API_Campaigns_Id(w http.ResponseWriter, r *http.Request) {
// API_Campaigns_Id_Results returns just the results for a given campaign to
// significantly reduce the information returned.
func API_Campaigns_Id_Results(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaigns_Id_Results(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))
@ -136,7 +127,7 @@ func API_Campaigns_Id_Results(w http.ResponseWriter, r *http.Request) {
}
// API_Campaigns_Id_Summary returns just the summary for a given campaign.
func API_Campaign_Id_Summary(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaign_Id_Summary(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64)
switch {
@ -157,7 +148,7 @@ func API_Campaign_Id_Summary(w http.ResponseWriter, r *http.Request) {
// API_Campaigns_Id_Complete effectively "ends" a campaign.
// Future phishing emails clicked will return a simple "404" page.
func API_Campaigns_Id_Complete(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Campaigns_Id_Complete(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64)
switch {
@ -173,7 +164,7 @@ func API_Campaigns_Id_Complete(w http.ResponseWriter, r *http.Request) {
// API_Groups returns a list of groups if requested via GET.
// If requested via POST, API_Groups creates a new group and returns a reference to it.
func API_Groups(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Groups(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
gs, err := models.GetGroups(ctx.Get(r, "user_id").(int64))
@ -208,7 +199,7 @@ func API_Groups(w http.ResponseWriter, r *http.Request) {
}
// API_Groups_Summary returns a summary of the groups owned by the current user.
func API_Groups_Summary(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Groups_Summary(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
@ -223,7 +214,7 @@ func API_Groups_Summary(w http.ResponseWriter, r *http.Request) {
// API_Groups_Id returns details about the requested group.
// If the group is not valid, API_Groups_Id returns null.
func API_Groups_Id(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Groups_Id(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))
@ -261,7 +252,7 @@ func API_Groups_Id(w http.ResponseWriter, r *http.Request) {
}
// API_Groups_Id_Summary returns a summary of the groups owned by the current user.
func API_Groups_Id_Summary(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Groups_Id_Summary(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
vars := mux.Vars(r)
@ -276,7 +267,7 @@ func API_Groups_Id_Summary(w http.ResponseWriter, r *http.Request) {
}
// API_Templates handles the functionality for the /api/templates endpoint
func API_Templates(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Templates(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
ts, err := models.GetTemplates(ctx.Get(r, "user_id").(int64))
@ -319,7 +310,7 @@ func API_Templates(w http.ResponseWriter, r *http.Request) {
}
// API_Templates_Id handles the functions for the /api/templates/:id endpoint
func API_Templates_Id(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Templates_Id(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))
@ -359,7 +350,7 @@ func API_Templates_Id(w http.ResponseWriter, r *http.Request) {
}
// API_Pages handles requests for the /api/pages/ endpoint
func API_Pages(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Pages(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
ps, err := models.GetPages(ctx.Get(r, "user_id").(int64))
@ -396,7 +387,7 @@ func API_Pages(w http.ResponseWriter, r *http.Request) {
// API_Pages_Id contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
// of a Page object
func API_Pages_Id(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Pages_Id(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))
@ -436,7 +427,7 @@ func API_Pages_Id(w http.ResponseWriter, r *http.Request) {
}
// API_SMTP handles requests for the /api/smtp/ endpoint
func API_SMTP(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_SMTP(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
ss, err := models.GetSMTPs(ctx.Get(r, "user_id").(int64))
@ -473,7 +464,7 @@ func API_SMTP(w http.ResponseWriter, r *http.Request) {
// API_SMTP_Id contains functions to handle the GET'ing, DELETE'ing, and PUT'ing
// of a SMTP object
func API_SMTP_Id(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_SMTP_Id(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))
@ -518,7 +509,7 @@ func API_SMTP_Id(w http.ResponseWriter, r *http.Request) {
}
// API_Import_Group imports a CSV of group members
func API_Import_Group(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Import_Group(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)
@ -530,7 +521,7 @@ func API_Import_Group(w http.ResponseWriter, r *http.Request) {
// API_Import_Email allows for the importing of email.
// Returns a Message object
func API_Import_Email(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Import_Email(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
return
@ -579,7 +570,7 @@ func API_Import_Email(w http.ResponseWriter, r *http.Request) {
// API_Import_Site 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 API_Import_Site(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Import_Site(w http.ResponseWriter, r *http.Request) {
cr := cloneRequest{}
if r.Method != "POST" {
JSONResponse(w, models.Response{Success: false, Message: "Method not allowed"}, http.StatusBadRequest)
@ -637,7 +628,7 @@ func API_Import_Site(w http.ResponseWriter, r *http.Request) {
// API_Send_Test_Email sends a test email using the template name
// and Target given.
func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
s := &models.EmailRequest{
ErrorChan: make(chan error),
UserId: ctx.Get(r, "user_id").(int64),
@ -735,7 +726,7 @@ func API_Send_Test_Email(w http.ResponseWriter, r *http.Request) {
}
}
// Send the test email
err = Worker.SendTestEmail(s)
err = as.worker.SendTestEmail(s)
if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)

+ 25
- 24
controllers/api_test.go View File

@ -11,41 +11,42 @@ import (
"github.com/gophish/gophish/config"
"github.com/gophish/gophish/models"
"github.com/gorilla/handlers"
"github.com/stretchr/testify/suite"
)
// ControllersSuite is a suite of tests to cover API related functions
type ControllersSuite struct {
suite.Suite
ApiKey string
ApiKey string
config *config.Config
adminServer *httptest.Server
phishServer *httptest.Server
}
// as is the Admin Server for our API calls
var as *httptest.Server = httptest.NewUnstartedServer(handlers.CombinedLoggingHandler(os.Stdout, CreateAdminRouter()))
// ps is the Phishing Server
var ps *httptest.Server = httptest.NewUnstartedServer(handlers.CombinedLoggingHandler(os.Stdout, CreatePhishingRouter()))
func (s *ControllersSuite) SetupSuite() {
config.Conf.DBName = "sqlite3"
config.Conf.DBPath = ":memory:"
config.Conf.MigrationsPath = "../db/db_sqlite3/migrations/"
err := models.Setup()
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)
// Setup the admin server for use in testing
as.Config.Addr = config.Conf.AdminConf.ListenURL
as.Start()
s.adminServer = httptest.NewUnstartedServer(NewAdminServer(s.config.AdminConf).server.Handler)
s.adminServer.Config.Addr = s.config.AdminConf.ListenURL
s.adminServer.Start()
// Get the API key to use for these tests
u, err := models.GetUser(1)
s.Nil(err)
s.ApiKey = u.ApiKey
// Start the phishing server
ps.Config.Addr = config.Conf.PhishConf.ListenURL
ps.Start()
s.phishServer = httptest.NewUnstartedServer(NewPhishingServer(s.config.PhishConf).server.Handler)
s.phishServer.Config.Addr = s.config.PhishConf.ListenURL
s.phishServer.Start()
// Move our cwd up to the project root for help with resolving
// static assets
err = os.Chdir("../")
@ -103,21 +104,21 @@ func (s *ControllersSuite) SetupTest() {
}
func (s *ControllersSuite) TestRequireAPIKey() {
resp, err := http.Post(fmt.Sprintf("%s/api/import/site", as.URL), "application/json", nil)
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.StatusBadRequest)
s.Equal(resp.StatusCode, http.StatusUnauthorized)
}
func (s *ControllersSuite) TestInvalidAPIKey() {
resp, err := http.Get(fmt.Sprintf("%s/api/groups/?api_key=%s", as.URL, "bogus-api-key"))
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.StatusBadRequest)
s.Equal(resp.StatusCode, http.StatusUnauthorized)
}
func (s *ControllersSuite) TestBearerToken() {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/groups/", as.URL), nil)
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)
@ -133,7 +134,7 @@ func (s *ControllersSuite) TestSiteImportBaseHref() {
}))
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", as.URL, s.ApiKey), "application/json",
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",
@ -150,8 +151,8 @@ func (s *ControllersSuite) TestSiteImportBaseHref() {
func (s *ControllersSuite) TearDownSuite() {
// Tear down the admin and phishing servers
as.Close()
ps.Close()
s.adminServer.Close()
san><span class="p">.phishServer.Close()
}
func TestControllerSuite(t *testing.T) {

+ 97
- 21
controllers/phish.go View File

@ -1,6 +1,8 @@
package controllers
import (
"compress/gzip"
"context"
"errors"
"fmt"
"net"
@ -8,11 +10,15 @@ import (
"strings"
"time"
"github.com/NYTimes/gziphandler"
"github.com/gophish/gophish/config"
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/handlers"
"github.com/gorilla/mux"
"github.com/jordan-wright/unindexed"
)
// ErrInvalidRequest is thrown when a request with an invalid structure is
@ -35,22 +41,91 @@ type TransparencyResponse struct {
// to return a transparency response.
const TransparencySuffix = "+"
// PhishingServerOption is a functional option that is used to configure the
// the phishing server
type PhishingServerOption func(*PhishingServer)
// PhishingServer is an HTTP server that implements the campaign event
// handlers, such as email open tracking, click tracking, and more.
type PhishingServer struct {
server *http.Server
config config.PhishServer
contactAddress string
}
// NewPhishingServer returns a new instance of the phishing server with
// provided options applied.
func NewPhishingServer(config config.PhishServer, options ...PhishingServerOption) *PhishingServer {
defaultServer := &http.Server{
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
Addr: config.ListenURL,
}
ps := &PhishingServer{
server: defaultServer,
config: config,
}
for _, opt := range options {
opt(ps)
}
ps.registerRoutes()
return ps
}
// WithContactAddress sets the contact address used by the transparency
// handlers
func WithContactAddress(addr string) PhishingServerOption {
return func(ps *PhishingServer) {
ps.contactAddress = addr
}
}
// Start launches the phishing server, listening on the configured address.
func (ps *PhishingServer) Start() error {
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)
}
// If TLS isn't configured, just listen on HTTP
log.Infof("Starting phishing server at http://%s", ps.config.ListenURL)
return ps.server.ListenAndServe()
}
// Shutdown attempts to gracefully shutdown the server.
func (ps *PhishingServer) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
return ps.server.Shutdown(ctx)
}
// CreatePhishingRouter creates the router that handles phishing connections.
func CreatePhishingRouter() http.Handler {
func (ps *PhishingServer) registerRoutes() {
router := mux.NewRouter()
fileServer := http.FileServer(UnindexedFileSystem{http.Dir("./static/endpoint/")})
fileServer := http.FileServer(unindexed.Dir("./static/endpoint/"))
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
router.HandleFunc("/track", PhishTracker)
router.HandleFunc("/robots.txt", RobotsHandler)
router.HandleFunc("/{path:.*}/track", PhishTracker)
router.HandleFunc("/{path:.*}/report", PhishReporter)
router.HandleFunc("/report", PhishReporter)
router.HandleFunc("/{path:.*}", PhishHandler)
return router
router.HandleFunc("/track", ps.TrackHandler)
router.HandleFunc("/robots.txt", ps.RobotsHandler)
router.HandleFunc("/{path:.*}/track", ps.TrackHandler)
router.HandleFunc("/{path:.*}/report", ps.ReportHandler)
router.HandleFunc("/report", ps.ReportHandler)
router.HandleFunc("/{path:.*}", ps.PhishHandler)
// Setup GZIP compression
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
phishHandler := gzipWrapper(router)
// Setup logging
phishHandler = handlers.CombinedLoggingHandler(log.Writer(), phishHandler)
ps.server.Handler = router
}
// PhishTracker tracks emails as they are opened, updating the status for the given Result
func PhishTracker(w http.ResponseWriter, r *http.Request) {
// TrackHandler tracks emails as they are opened, updating the status for the given Result
func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
err, r := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
@ -71,7 +146,7 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
// Check for a transparency request
if strings.HasSuffix(rid, TransparencySuffix) {
TransparencyHandler(w, r)
ps.TransparencyHandler(w, r)
return
}
@ -82,8 +157,8 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "static/images/pixel.png")
}
// PhishReporter tracks emails as they are reported, updating the status for the given Result
func PhishReporter(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) {
err, r := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
@ -104,7 +179,7 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
// Check for a transparency request
if strings.HasSuffix(rid, TransparencySuffix) {
TransparencyHandler(w, r)
ps.TransparencyHandler(w, r)
return
}
@ -117,7 +192,7 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
// PhishHandler handles incoming client connections and registers the associated actions performed
// (such as clicked link, etc.)
func PhishHandler(w http.ResponseWriter, r *http.Request) {
func (ps *PhishingServer) PhishHandler(w http.ResponseWriter, r *http.Request) {
err, r := setupContext(r)
if err != nil {
// Log the error if it wasn't something we can safely ignore
@ -152,7 +227,7 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
// Check for a transparency request
if strings.HasSuffix(rid, TransparencySuffix) {
TransparencyHandler(w, r)
ps.TransparencyHandler(w, r)
return
}
@ -211,23 +286,24 @@ func renderPhishResponse(w http.ResponseWriter, r *http.Request, ptx models.Phis
}
// RobotsHandler prevents search engines, etc. from indexing phishing materials
func RobotsHandler(w http.ResponseWriter, r *http.Request) {
func (ps *PhishingServer) RobotsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "User-agent: *\nDisallow: /")
}
// TransparencyHandler returns a TransparencyResponse for the provided result
// and campaign.
func TransparencyHandler(w http.ResponseWriter, r *http.Request) {
func (ps *PhishingServer) TransparencyHandler(w http.ResponseWriter, r *http.Request) {
rs := ctx.Get(r, "result").(models.Result)
tr := &TransparencyResponse{
Server: config.ServerName,
SendDate: rs.SendDate,
ContactAddress: config.Conf.ContactAddress,
ContactAddress: ps.contactAddress,
}
JSONResponse(w, tr, http.StatusOK)
}
// setupContext handles some of the administrative work around receiving a new request, such as checking the result ID, the campaign, etc.
// setupContext handles some of the administrative work around receiving a new
// request, such as checking the result ID, the campaign, etc.
func setupContext(r *http.Request) (error, *http.Request) {
err := r.ParseForm()
if err != nil {

+ 12
- 12
controllers/phish_test.go View File

@ -38,7 +38,7 @@ func (s *ControllersSuite) getFirstEmailRequest() models.EmailRequest {
}
func (s *ControllersSuite) openEmail(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
@ -49,19 +49,19 @@ func (s *ControllersSuite) openEmail(rid string) {
}
func (s *ControllersSuite) reportedEmail(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNoContent)
}
func (s *ControllersSuite) reportEmail404(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/report?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNotFound)
}
func (s *ControllersSuite) openEmail404(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/track?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
defer resp.Body.Close()
s.Nil(err)
@ -69,7 +69,7 @@ func (s *ControllersSuite) openEmail404(rid string) {
}
func (s *ControllersSuite) clickLink(rid string, expectedHTML string) {
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
@ -79,7 +79,7 @@ func (s *ControllersSuite) clickLink(rid string, expectedHTML string) {
}
func (s *ControllersSuite) clickLink404(rid string) {
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s/?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, rid))
s.Nil(err)
defer resp.Body.Close()
s.Nil(err)
@ -87,14 +87,14 @@ func (s *ControllersSuite) clickLink404(rid string) {
}
func (s *ControllersSuite) transparencyRequest(r models.Result, rid, path string) {
resp, err := http.Get(fmt.Sprintf("%s%s?%s=%s", ps.URL, path, models.RecipientParameter, rid))
resp, err := http.Get(fmt.Sprintf("%s%s?%s=%s", san><span class="p">.phishServer.URL, path, models.RecipientParameter, rid))
s.Nil(err)
defer resp.Body.Close()
s.Equal(resp.StatusCode, http.StatusOK)
tr := &TransparencyResponse{}
err = json.NewDecoder(resp.Body).Decode(tr)
s.Nil(err)
s.Equal(tr.ContactAddress, config.Conf.ContactAddress)
s.Equal(tr.ContactAddress, s.config.ContactAddress)
s.Equal(tr.SendDate, r.SendDate)
s.Equal(tr.Server, config.ServerName)
}
@ -146,11 +146,11 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
}
func (s *ControllersSuite) TestNoRecipientID() {
resp, err := http.Get(fmt.Sprintf("%s/track", ps.URL))
resp, err := http.Get(fmt.Sprintf("%s/track", san><span class="p">.phishServer.URL))
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNotFound)
resp, err = http.Get(ps.URL)
resp, err = http.Get(san><span class="p">.phishServer.URL)
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNotFound)
}
@ -183,7 +183,7 @@ func (s *ControllersSuite) TestCompletedCampaignClick() {
func (s *ControllersSuite) TestRobotsHandler() {
expected := []byte("User-agent: *\nDisallow: /\n")
resp, err := http.Get(fmt.Sprintf("%s/robots.txt", ps.URL))
resp, err := http.Get(fmt.Sprintf("%s/robots.txt", san><span class="p">.phishServer.URL))
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusOK)
defer resp.Body.Close()
@ -259,7 +259,7 @@ func (s *ControllersSuite) TestRedirectTemplating() {
},
}
result := campaign.Results[0]
resp, err := client.PostForm(fmt.Sprintf("%s/?%s=%s", ps.URL, models.RecipientParameter, result.RId), url.Values{"username": {"test"}, "password": {"test"}})
resp, err := client.PostForm(fmt.Sprintf("%s/?%s=%s", san><span class="p">.phishServer.URL, models.RecipientParameter, result.RId), url.Values{"username": {"test"}, "password": {"test"}})
s.Nil(err)
defer resp.Body.Close()
s.Equal(http.StatusFound, resp.StatusCode)

+ 166
- 133
controllers/route.go View File

@ -1,72 +1,153 @@
package controllers
import (
"fmt"
"compress/gzip"
"context"
"html/template"
"net/http"
"net/url"
"time"
"github.com/NYTimes/gziphandler"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/config"
ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger"
mid "github.com/gophish/gophish/middleware"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gophish/gophish/worker"
"github.com/gorilla/csrf"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/jordan-wright/unindexed"
)
// CreateAdminRouter creates the routes for handling requests to the web interface.
// AdminServerOption is a functional option that is used to configure the
// admin server
type AdminServerOption func(*AdminServer)
// AdminServer is an HTTP server that implements the administrative Gophish
// handlers, including the dashboard and REST API.
type AdminServer struct {
server *http.Server
worker worker.Worker
config config.AdminServer
}
// WithWorker is an option that sets the background worker.
func WithWorker(w worker.Worker) AdminServerOption {
return func(as *AdminServer) {
as.worker = w
}
}
// NewAdminServer returns a new instance of the AdminServer with the
// provided config and options applied.
func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *AdminServer {
defaultWorker, _ := worker.New()
defaultServer := &http.Server{
ReadTimeout: 10 * time.Second,
Addr: config.ListenURL,
}
as := &AdminServer{
worker: defaultWorker,
server: defaultServer,
config: config,
}
for _, opt := range options {
opt(as)
}
as.registerRoutes()
return as
}
// Start launches the admin server, listening on the configured address.
func (as *AdminServer) Start() error {
if as.worker != nil {
go as.worker.Start()
}
if as.config.UseTLS {
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)
}
// If TLS isn't configured, just listen on HTTP
log.Infof("Starting admin server at http://%s", as.config.ListenURL)
return as.server.ListenAndServe()
}
// Shutdown attempts to gracefully shutdown the server.
func (as *AdminServer) Shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
return as.server.Shutdown(ctx)
}
// SetupAdminRoutes creates the routes for handling requests to the web interface.
// This function returns an http.Handler to be used in http.ListenAndServe().
func CreateAdminRouter() http.Handler {
func (as *AdminServer) registerRoutes() {
router := mux.NewRouter()
// Base Front-end routes
router.HandleFunc("/", Use(Base, mid.RequireLogin))
router.HandleFunc("/login", Login)
router.HandleFunc("/logout", Use(Logout, mid.RequireLogin))
router.HandleFunc("/campaigns", Use(Campaigns, mid.RequireLogin))
router.HandleFunc("/campaigns/{id:[0-9]+}", Use(CampaignID, mid.RequireLogin))
router.HandleFunc("/templates", Use(Templates, mid.RequireLogin))
router.HandleFunc("/users", Use(Users, mid.RequireLogin))
router.HandleFunc("/landing_pages", Use(LandingPages, mid.RequireLogin))
router.HandleFunc("/sending_profiles", Use(SendingProfiles, mid.RequireLogin))
router.HandleFunc("/register", Use(Register, mid.RequireLogin))
router.HandleFunc("/settings", Use(Settings, mid.RequireLogin))
router.HandleFunc("/", 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("/register", Use(as.Register, mid.RequireLogin))
router.HandleFunc("/settings", Use(as.Settings, mid.RequireLogin))
// Create the API routes
api := router.PathPrefix("/api").Subrouter()
api = api.StrictSlash(true)
api.HandleFunc("/reset", Use(API_Reset, mid.RequireAPIKey))
api.HandleFunc("/campaigns/", Use(API_Campaigns, mid.RequireAPIKey))
api.HandleFunc("/campaigns/summary", Use(API_Campaigns_Summary, mid.RequireAPIKey))
api.HandleFunc("/campaigns/{id:[0-9]+}", Use(API_Campaigns_Id, mid.RequireAPIKey))
api.HandleFunc("/campaigns/{id:[0-9]+}/results", Use(API_Campaigns_Id_Results, mid.RequireAPIKey))
api.HandleFunc("/campaigns/{id:[0-9]+}/summary", Use(API_Campaign_Id_Summary, mid.RequireAPIKey))
api.HandleFunc("/campaigns/{id:[0-9]+}/complete", Use(API_Campaigns_Id_Complete, mid.RequireAPIKey))
api.HandleFunc("/groups/", Use(API_Groups, mid.RequireAPIKey))
api.HandleFunc("/groups/summary", Use(API_Groups_Summary, mid.RequireAPIKey))
api.HandleFunc("/groups/{id:[0-9]+}", Use(API_Groups_Id, mid.RequireAPIKey))
api.HandleFunc("/groups/{id:[0-9]+}/summary", Use(API_Groups_Id_Summary, mid.RequireAPIKey))
api.HandleFunc("/templates/", Use(API_Templates, mid.RequireAPIKey))
api.HandleFunc("/templates/{id:[0-9]+}", Use(API_Templates_Id, mid.RequireAPIKey))
api.HandleFunc("/pages/", Use(API_Pages, mid.RequireAPIKey))
api.HandleFunc("/pages/{id:[0-9]+}", Use(API_Pages_Id, mid.RequireAPIKey))
api.HandleFunc("/smtp/", Use(API_SMTP, mid.RequireAPIKey))
api.HandleFunc("/smtp/{id:[0-9]+}", Use(API_SMTP_Id, mid.RequireAPIKey))
api.HandleFunc("/util/send_test_email", Use(API_Send_Test_Email, mid.RequireAPIKey))
api.HandleFunc("/import/group", Use(API_Import_Group, mid.RequireAPIKey))
api.HandleFunc("/import/email", Use(API_Import_Email, mid.RequireAPIKey))
api.HandleFunc("/import/site", Use(API_Import_Site, mid.RequireAPIKey))
api.Use(mid.RequireAPIKey)
api.HandleFunc("/reset", as.API_Reset)
api.HandleFunc("/campaigns/", as.API_Campaigns)
api.HandleFunc("/campaigns/summary", as.API_Campaigns_Summary)
api.HandleFunc("/campaigns/{id:[0-9]+}", as.API_Campaigns_Id)
api.HandleFunc("/campaigns/{id:[0-9]+}/results", as.API_Campaigns_Id_Results)
api.HandleFunc("/campaigns/{id:[0-9]+}/summary", as.API_Campaign_Id_Summary)
api.HandleFunc("/campaigns/{id:[0-9]+}/complete", as.API_Campaigns_Id_Complete)
api.HandleFunc("/groups/", as.API_Groups)
api.HandleFunc("/groups/summary", as.API_Groups_Summary)
api.HandleFunc("/groups/{id:[0-9]+}", as.API_Groups_Id)
api.HandleFunc("/groups/{id:[0-9]+}/summary", as.API_Groups_Id_Summary)
api.HandleFunc("/templates/", as.API_Templates)
api.HandleFunc("/templates/{id:[0-9]+}", as.API_Templates_Id)
api.HandleFunc("/pages/", as.API_Pages)
api.HandleFunc("/pages/{id:[0-9]+}", as.API_Pages_Id)
api.HandleFunc("/smtp/", as.API_SMTP)
api.HandleFunc("/smtp/{id:[0-9]+}", as.API_SMTP_Id)
api.HandleFunc("/util/send_test_email", as.API_Send_Test_Email)
api.HandleFunc("/import/group", as.API_Import_Group)
api.HandleFunc("/import/email", as.API_Import_Email)
api.HandleFunc("/import/site", as.API_Import_Site)
// Setup static file serving
router.PathPrefix("/").Handler(http.FileServer(UnindexedFileSystem{http.Dir("./static/")}))
router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("./static/")))
// Setup CSRF Protection
csrfHandler := csrf.Protect([]byte(auth.GenerateSecureKey()),
csrf.FieldName("csrf_token"),
csrf.Secure(config.Conf.AdminConf.UseTLS))
csrfRouter := csrfHandler(router)
return Use(csrfRouter.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
csrf.Secure(as.config.UseTLS))
adminHandler := csrfHandler(router)
adminHandler = Use(adminHandler.ServeHTTP, mid.CSRFExceptions, mid.GetContext)
// Setup GZIP compression
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
adminHandler = gzipWrapper(adminHandler)
// Setup logging
adminHandler = handlers.CombinedLoggingHandler(log.Writer(), adminHandler)
as.server.Handler = adminHandler
}
// Use allows us to stack middleware to process the request
@ -78,16 +159,29 @@ func Use(handler http.HandlerFunc, mid ...func(http.Handler) http.HandlerFunc) h
return handler
}
type templateParams struct {
Title string
Flashes []interface{}
User models.User
Token string
Version string
}
// newTemplateParams returns the default template parameters for a user and
// the CSRF token.
func newTemplateParams(r *http.Request) templateParams {
return templateParams{
Token: csrf.Token(r),
User: ctx.Get(r, "user").(models.User),
Version: config.Version,
}
}
// Register creates a new user
func Register(w http.ResponseWriter, r *http.Request) {
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 := struct {
Title string
Flashes []interface{}
User models.User
Token string
}{Title: "Register", Token: csrf.Token(r)}
params := templateParams{Title: "Register", Token: csrf.Token(r)}
session := ctx.Get(r, "session").(*sessions.Session)
switch {
case r.Method == "GET":
@ -120,99 +214,60 @@ func Register(w http.ResponseWriter, r *http.Request) {
}
// Base handles the default path and template execution
func Base(w http.ResponseWriter, r *http.Request) {
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Dashboard", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) Base(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Dashboard"
getTemplate(w, "dashboard").ExecuteTemplate(w, "base", params)
}
// Campaigns handles the default path and template execution
func Campaigns(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Campaigns", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) Campaigns(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Campaigns"
getTemplate(w, "campaigns").ExecuteTemplate(w, "base", params)
}
// CampaignID handles the default path and template execution
func CampaignID(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Campaign Results", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) CampaignID(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Campaign Results"
getTemplate(w, "campaign_results").ExecuteTemplate(w, "base", params)
}
// Templates handles the default path and template execution
func Templates(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Email Templates", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) Templates(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Email Templates"
getTemplate(w, "templates").ExecuteTemplate(w, "base", params)
}
// Users handles the default path and template execution
func Users(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Users & Groups", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) Users(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Users & Groups"
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
}
// LandingPages handles the default path and template execution
func LandingPages(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Landing Pages", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) LandingPages(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Landing Pages"
getTemplate(w, "landing_pages").ExecuteTemplate(w, "base", params)
}
// SendingProfiles handles the default path and template execution
func SendingProfiles(w http.ResponseWriter, r *http.Request) {
// Example of using session - will be removed.
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
}{Title: "Sending Profiles", User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
func (as *AdminServer) SendingProfiles(w http.ResponseWriter, r *http.Request) {
params := newTemplateParams(r)
params.Title = "Sending Profiles"
getTemplate(w, "sending_profiles").ExecuteTemplate(w, "base", params)
}
// Settings handles the changing of settings
func Settings(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
params := struct {
User models.User
Title string
Flashes []interface{}
Token string
Version string
}{Title: "Settings", Version: config.Version, User: ctx.Get(r, "user").(models.User), Token: csrf.Token(r)}
params := newTemplateParams(r)
params.Title = "Settings"
getTemplate(w, "settings").ExecuteTemplate(w, "base", params)
case r.Method == "POST":
err := auth.ChangePassword(r)
@ -235,7 +290,7 @@ func Settings(w http.ResponseWriter, r *http.Request) {
// Login handles the authentication flow for a user. If credentials are valid,
// a session is created
func Login(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
params := struct {
User models.User
Title string
@ -289,7 +344,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
}
// Logout destroys the current user session
func Logout(w http.ResponseWriter, r *http.Request) {
func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) {
session := ctx.Get(r, "session").(*sessions.Session)
delete(session.Values, "id")
Flash(w, r, "success", "You have successfully logged out")
@ -297,28 +352,6 @@ func Logout(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", 302)
}
// Preview allows for the viewing of page html in a separate browser window
func Preview(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "%s", r.FormValue("html"))
}
// Clone takes a URL as a POST parameter and returns the site HTML
func Clone(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusBadRequest)
return
}
if url, ok := vars["url"]; ok {
log.Error(url)
}
http.Error(w, "No URL given.", http.StatusBadRequest)
}
func getTemplate(w http.ResponseWriter, tmpl string) *template.Template {
templates := template.New("template")
_, err := templates.ParseFiles("templates/base.html", "templates/"+tmpl+".html", "templates/flashes.html")

+ 7
- 7
controllers/route_test.go View File

@ -10,7 +10,7 @@ import (
)
func (s *ControllersSuite) TestLoginCSRF() {
resp, err := http.PostForm(fmt.Sprintf("%s/login", as.URL),
resp, err := http.PostForm(fmt.Sprintf("%s/login", s.adminServer.URL),
url.Values{
"username": {"admin"},
"password": {"gophish"},
@ -21,7 +21,7 @@ func (s *ControllersSuite) TestLoginCSRF() {
}
func (s *ControllersSuite) TestInvalidCredentials() {
resp, err := http.Get(fmt.Sprintf("%s/login", as.URL))
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
s.Equal(err, nil)
s.Equal(resp.StatusCode, http.StatusOK)
@ -32,7 +32,7 @@ func (s *ControllersSuite) TestInvalidCredentials() {
s.Equal(ok, true)
client := &http.Client{}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", as.URL), strings.NewReader(url.Values{
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", s.adminServer.URL), strings.NewReader(url.Values{
"username": {"admin"},
"password": {"invalid"},
"csrf_token": {token},
@ -48,7 +48,7 @@ func (s *ControllersSuite) TestInvalidCredentials() {
}
func (s *ControllersSuite) TestSuccessfulLogin() {
resp, err := http.Get(fmt.Sprintf("%s/login", as.URL))
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
s.Equal(err, nil)
s.Equal(resp.StatusCode, http.StatusOK)
@ -59,7 +59,7 @@ func (s *ControllersSuite) TestSuccessfulLogin() {
s.Equal(ok, true)
client := &http.Client{}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", as.URL), strings.NewReader(url.Values{
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login", s.adminServer.URL), strings.NewReader(url.Values{
"username": {"admin"},
"password": {"gophish"},
"csrf_token": {token},
@ -76,7 +76,7 @@ func (s *ControllersSuite) TestSuccessfulLogin() {
func (s *ControllersSuite) TestSuccessfulRedirect() {
next := "/campaigns"
resp, err := http.Get(fmt.Sprintf("%s/login", as.URL))
resp, err := http.Get(fmt.Sprintf("%s/login", s.adminServer.URL))
s.Equal(err, nil)
s.Equal(resp.StatusCode, http.StatusOK)
@ -91,7 +91,7 @@ func (s *ControllersSuite) TestSuccessfulRedirect() {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login?next=%s", as.URL, next), strings.NewReader(url.Values{
req, err := http.NewRequest("POST", fmt.Sprintf("%s/login?next=%s", s.adminServer.URL, next), strings.NewReader(url.Values{
"username": {"admin"},
"password": {"gophish"},
"csrf_token": {token},

+ 0
- 35
controllers/static.go View File

@ -1,35 +0,0 @@
package controllers
import (
"net/http"
"strings"
)
// UnindexedFileSystem is an implementation of a standard http.FileSystem
// without the ability to list files in the directory.
// This implementation is largely inspired by
// https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings
type UnindexedFileSystem struct {
fs http.FileSystem
}
// Open returns a file from the static directory. If the requested path ends
// with a slash, there is a check for an index.html file. If none exists, then
// an error is returned.
func (ufs UnindexedFileSystem) Open(name string) (http.File, error) {
f, err := ufs.fs.Open(name)
if err != nil {
return nil, err
}
s, err := f.Stat()
if s.IsDir() {
index := strings.TrimSuffix(name, "/") + "/index.html"
indexFile, err := ufs.fs.Open(index)
if err != nil {
return nil, err
}
return indexFile, nil
}
return f, nil
}

+ 0
- 81
controllers/static_test.go View File

@ -1,81 +0,0 @@
package controllers
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
)
var fileContent = []byte("Hello world")
func mustRemoveAll(dir string) {
err := os.RemoveAll(dir)
if err != nil {
panic(err)
}
}
func createTestFile(dir, filename string) error {
return ioutil.WriteFile(filepath.Join(dir, filename), fileContent, 0644)
}
func (s *ControllersSuite) TestGetStaticFile() {
dir, err := ioutil.TempDir("static/endpoint", "test-")
tempFolder := filepath.Base(dir)
s.Nil(err)
defer mustRemoveAll(dir)
err = createTestFile(dir, "foo.txt")
s.Nil(nil, err)
resp, err := http.Get(fmt.Sprintf("%s/static/%s/foo.txt", ps.URL, tempFolder))
s.Nil(err)
defer resp.Body.Close()
got, err := ioutil.ReadAll(resp.Body)
s.Nil(err)
s.Equal(bytes.Compare(fileContent, got), 0, fmt.Sprintf("Got %s", got))
}
func (s *ControllersSuite) TestStaticFileListing() {
dir, err := ioutil.TempDir("static/endpoint", "test-")
tempFolder := filepath.Base(dir)
s.Nil(err)
defer mustRemoveAll(dir)
err = createTestFile(dir, "foo.txt")
s.Nil(nil, err)
resp, err := http.Get(fmt.Sprintf("%s/static/%s/", ps.URL, tempFolder))
s.Nil(err)
defer resp.Body.Close()
s.Nil(err)
s.Equal(resp.StatusCode, http.StatusNotFound)
}
func (s *ControllersSuite) TestStaticIndex() {
dir, err := ioutil.TempDir("static/endpoint", "test-")
tempFolder := filepath.Base(dir)
s.Nil(err)
defer mustRemoveAll(dir)
err = createTestFile(dir, "index.html")
s.Nil(nil, err)
resp, err := http.Get(fmt.Sprintf("%s/static/%s/", ps.URL, tempFolder))
s.Nil(err)
defer resp.Body.Close()
got, err := ioutil.ReadAll(resp.Body)
s.Nil(err)
s.Equal(bytes.Compare(fileContent, got), 0, fmt.Sprintf("Got %s", got))
}

+ 28
- 53
gophish.go View File

@ -26,24 +26,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import (
"compress/gzip"
"context"
"io/ioutil"
"net/http"
"os"
"sync"
"os/signal"
"gopkg.in/alecthomas/kingpin.v2"
"github.com/NYTimes/gziphandler"
"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/mailer"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gorilla/handlers"
)
var (
@ -65,31 +58,25 @@ func main() {
kingpin.Parse()
// Load the config
err = config.LoadConfig(*configPath)
conf, err := config.LoadConfig(*configPath)
// Just warn if a contact address hasn't been configured
if err != nil {
log.Fatal(err)
}
if config.Conf.ContactAddress == "" {
if conf.ContactAddress == "" {
log.Warnf("No contact address has been configured.")
log.Warnf("Please consider adding a contact_address entry in your config.json")
}
config.Version = string(version)
err = log.Setup()
err = log.Setup(conf)
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Provide the option to disable the built-in mailer
if !*disableMailer {
go mailer.Mailer.Start(ctx)
}
// Setup the global variables and settings
err = models.Setup()
err = models.Setup(conf)
if err != nil {
log.Fatal(err)
}
@ -99,39 +86,27 @@ func main() {
if err != nil {
log.Fatal(err)
}
wg := &sync.WaitGroup{}
wg.Add(1)
// Start the web servers
go func() {
defer wg.Done()
gzipWrapper, _ := gziphandler.NewGzipLevelHandler(gzip.BestCompression)
adminHandler := gzipWrapper(controllers.CreateAdminRouter())
auth.Store.Options.Secure = config.Conf.AdminConf.UseTLS
if config.Conf.AdminConf.UseTLS { // use TLS for Admin web server if available
err := util.CheckAndCreateSSL(config.Conf.AdminConf.CertPath, config.Conf.AdminConf.KeyPath)
if err != nil {
log.Fatal(err)
}
log.Infof("Starting admin server at https://%s", config.Conf.AdminConf.ListenURL)
log.Info(http.ListenAndServeTLS(config.Conf.AdminConf.ListenURL, config.Conf.AdminConf.CertPath, config.Conf.AdminConf.KeyPath,