gophish/models/models.go

254 lines
6.9 KiB
Go

package models
import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"os"
"time"
"bitbucket.org/liamstask/goose/lib/goose"
mysql "github.com/go-sql-driver/mysql"
"github.com/gophish/gophish/auth"
"github.com/gophish/gophish/config"
log "github.com/gophish/gophish/logger"
"github.com/jinzhu/gorm"
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
)
var db *gorm.DB
var conf *config.Config
const MaxDatabaseConnectionAttempts int = 10
// DefaultAdminUsername is the default username for the administrative user
const DefaultAdminUsername = "admin"
// InitialAdminPassword is the environment variable that specifies which
// password to use for the initial root login instead of generating one
// randomly
const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD"
// InitialAdminApiToken is the environment variable that specifies the
// API token to seed the initial root login instead of generating one
// randomly
const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN"
const (
CampaignInProgress string = "In progress"
CampaignQueued string = "Queued"
CampaignCreated string = "Created"
CampaignEmailsSent string = "Emails Sent"
CampaignComplete string = "Completed"
EventSent string = "Email Sent"
EventSendingError string = "Error Sending Email"
EventOpened string = "Email Opened"
EventClicked string = "Clicked Link"
EventDataSubmit string = "Submitted Data"
EventReported string = "Email Reported"
EventProxyRequest string = "Proxied request"
StatusSuccess string = "Success"
StatusQueued string = "Queued"
StatusSending string = "Sending"
StatusUnknown string = "Unknown"
StatusScheduled string = "Scheduled"
StatusRetry string = "Retrying"
Error string = "Error"
)
// Flash is used to hold flash information for use in templates.
type Flash struct {
Type string
Message string
}
// Response contains the attributes found in an API response
type Response struct {
Message string `json:"message"`
Success bool `json:"success"`
Data interface{} `json:"data"`
}
// Copy of auth.GenerateSecureKey to prevent cyclic import with auth library
func generateSecureKey() string {
k := make([]byte, 32)
io.ReadFull(rand.Reader, k)
return fmt.Sprintf("%x", k)
}
func chooseDBDriver(name, openStr string) goose.DBDriver {
d := goose.DBDriver{Name: name, OpenStr: openStr}
switch name {
case "mysql":
d.Import = "github.com/go-sql-driver/mysql"
d.Dialect = &goose.MySqlDialect{}
// Default database is sqlite3
default:
d.Import = "github.com/mattn/go-sqlite3"
d.Dialect = &goose.Sqlite3Dialect{}
}
return d
}
func createTemporaryPassword(u *User) error {
var temporaryPassword string
if envPassword := os.Getenv(InitialAdminPassword); envPassword != "" {
temporaryPassword = envPassword
} else {
// This will result in a 16 character password which could be viewed as an
// inconvenience, but it should be ok for now.
temporaryPassword = auth.GenerateSecureKey(auth.MinPasswordLength)
}
hash, err := auth.GeneratePasswordHash(temporaryPassword)
if err != nil {
return err
}
u.Hash = hash
// Anytime a temporary password is created, we will force the user
// to change their password
u.PasswordChangeRequired = true
err = db.Save(u).Error
if err != nil {
return err
}
log.Infof("Please login with the username admin and the password %s", temporaryPassword)
return nil
}
// Setup initializes the database and runs any needed migrations.
//
// First, it establishes a connection to the database, then runs any migrations
// newer than the version the database is on.
//
// Once the database is up-to-date, we create an admin user (if needed) that
// has a randomly generated API key and password.
func Setup(c *config.Config) error {
// Setup the package-scoped config
conf = c
// Setup the goose configuration
migrateConf := &goose.DBConf{
MigrationsDir: conf.MigrationsPath,
Env: "production",
Driver: chooseDBDriver(conf.DBName, conf.DBPath),
}
// Get the latest possible migration
latest, err := goose.GetMostRecentDBVersion(migrateConf.MigrationsDir)
if err != nil {
log.Error(err)
return err
}
// Register certificates for tls encrypted db connections
if conf.DBSSLCaPath != "" {
switch conf.DBName {
case "mysql":
rootCertPool := x509.NewCertPool()
pem, err := ioutil.ReadFile(conf.DBSSLCaPath)
if err != nil {
log.Error(err)
return err
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
log.Error("Failed to append PEM.")
return err
}
mysql.RegisterTLSConfig("ssl_ca", &tls.Config{
RootCAs: rootCertPool,
})
// Default database is sqlite3, which supports no tls, as connection
// is file based
default:
}
}
// Open our database connection
i := 0
for {
db, err = gorm.Open(conf.DBName, conf.DBPath)
if err == nil {
break
}
if err != nil && i >= MaxDatabaseConnectionAttempts {
log.Error(err)
return err
}
i += 1
log.Warn("waiting for database to be up...")
time.Sleep(5 * time.Second)
}
db.LogMode(false)
db.SetLogger(log.Logger)
db.DB().SetMaxOpenConns(1)
if err != nil {
log.Error(err)
return err
}
// Migrate up to the latest version
err = goose.RunMigrationsOnDb(migrateConf, migrateConf.MigrationsDir, latest, db.DB())
if err != nil {
log.Error(err)
return err
}
// Create the admin user if it doesn't exist
var userCount int64
var adminUser User
db.Model(&User{}).Count(&userCount)
adminRole, err := GetRoleBySlug(RoleAdmin)
if err != nil {
log.Error(err)
return err
}
if userCount == 0 {
adminUser := User{
Username: DefaultAdminUsername,
Role: adminRole,
RoleID: adminRole.ID,
PasswordChangeRequired: true,
}
if envToken := os.Getenv(InitialAdminApiToken); envToken != "" {
adminUser.ApiKey = envToken
} else {
adminUser.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
}
err = db.Save(&adminUser).Error
if err != nil {
log.Error(err)
return err
}
}
// If this is the first time the user is installing Gophish, then we will
// generate a temporary password for the admin user.
//
// We do this here instead of in the block above where the admin is created
// since there's the chance the user executes Gophish and has some kind of
// error, then tries restarting it. If they didn't grab the password out of
// the logs, then they would have lost it.
//
// By doing the temporary password here, we will regenerate that temporary
// password until the user is able to reset the admin password.
if adminUser.Username == "" {
adminUser, err = GetUserByUsername(DefaultAdminUsername)
if err != nil {
log.Error(err)
return err
}
}
if adminUser.PasswordChangeRequired {
err = createTemporaryPassword(&adminUser)
if err != nil {
log.Error(err)
return err
}
}
return nil
}