mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-30 15:38:49 +02:00
add path param lure customization
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
@@ -64,6 +64,15 @@ type Campaign struct {
|
||||
WebhookIncludeData string `gorm:"not null;default:'full'"`
|
||||
WebhookEvents int `gorm:"not null;default:0"`
|
||||
|
||||
// LureURLMode, LureCodeAlgo and LureCodeLength are snapshotted from the
|
||||
// campaign template when the campaign is scheduled. Reading them from the
|
||||
// campaign rather than the template keeps recipients added later to a self
|
||||
// managed campaign consistent with the ones already scheduled, and keeps a
|
||||
// template edit from changing the form of URLs already delivered.
|
||||
LureURLMode string `gorm:"not null;default:'query'"`
|
||||
LureCodeAlgo string `gorm:"not null;default:'crockford32'"`
|
||||
LureCodeLength int `gorm:"not null;default:12"`
|
||||
|
||||
// has one
|
||||
CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"`
|
||||
CampaignTemplate *CampaignTemplate
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,8 +46,70 @@ type CampaignRecipient struct {
|
||||
// NotableEventID is the most notable event for this recipient
|
||||
NotableEvent *Event `gorm:"foreignKey:NotableEventID;references:ID"`
|
||||
NotableEventID *uuid.UUID `gorm:"type:uuid;index"`
|
||||
|
||||
// LureCode is the short identifier that appears in the lure URL instead of
|
||||
// the campaign recipient UUID, for example https://example.com/4H7K9QM2XR3T.
|
||||
// It is stored exactly as written and matched exactly, which is what lets an
|
||||
// operator pick a code such as Special-42 and get that link verbatim.
|
||||
//
|
||||
// Releasing a code sets this back to null. Nulling rather than flagging is
|
||||
// what keeps a reclaimed code from still showing against its former owner,
|
||||
// and it lets the unique index below need nothing but IS NOT NULL.
|
||||
LureCode *string `gorm:"type:varchar(64)"`
|
||||
// LureCodeCustom marks a code the operator set by hand. A custom code cannot
|
||||
// be told apart from a generated one by shape, so the distinction is stored.
|
||||
LureCodeCustom bool `gorm:"not null;default:false"`
|
||||
}
|
||||
|
||||
func (CampaignRecipient) TableName() string {
|
||||
return CAMPAIGN_RECIPIENT_TABLE_NAME
|
||||
}
|
||||
|
||||
// Migrate creates the partial unique index backing lure code allocation.
|
||||
//
|
||||
// The predicate has to exclude rows carrying no code, otherwise every recipient
|
||||
// in a query mode campaign enters the index under the same value and the second
|
||||
// insert fails. Because releasing a code nulls it, that single predicate also
|
||||
// covers reuse: a released code leaves the index and is free to claim again.
|
||||
//
|
||||
// This is raw SQL rather than a gorm index tag because only the sqlite driver
|
||||
// emits the WHERE clause. The generic migrator drops it, which would silently
|
||||
// produce a full unique index and break reuse.
|
||||
func (CampaignRecipient) Migrate(db *gorm.DB) error {
|
||||
// lure_code is the only code column this table carries. AutoMigrate adds
|
||||
// columns but never drops them, so any other lure column and its indexes are
|
||||
// removed here. errors are ignored because on a fresh install there is
|
||||
// nothing to drop.
|
||||
db.Exec(`DROP INDEX IF EXISTS idx_campaign_recipients_lure_code_lookup`)
|
||||
db.Exec(`DROP INDEX IF EXISTS idx_campaign_recipients_lure_code_released_at`)
|
||||
db.Exec(`DROP INDEX IF EXISTS idx_campaign_recipients_lure_code_folded`)
|
||||
db.Exec(`ALTER TABLE campaign_recipients DROP COLUMN lure_code_lookup`)
|
||||
db.Exec(`ALTER TABLE campaign_recipients DROP COLUMN lure_code_released_at`)
|
||||
db.Exec(`ALTER TABLE campaign_recipients DROP COLUMN lure_code_folded`)
|
||||
|
||||
createIndex := func() error {
|
||||
return db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_campaign_recipients_lure_code
|
||||
ON campaign_recipients (lure_code)
|
||||
WHERE lure_code IS NOT NULL`).Error
|
||||
}
|
||||
err := createIndex()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// the index fails when two rows already hold the same code. keep the first
|
||||
// row for each code and release the rest, then build the index.
|
||||
//
|
||||
// this runs only after a failure, because the scan and grouping it needs
|
||||
// are too costly to pay on every startup.
|
||||
if dedupErr := db.Exec(`UPDATE campaign_recipients SET lure_code = NULL
|
||||
WHERE lure_code IS NOT NULL
|
||||
AND rowid NOT IN (
|
||||
SELECT MIN(rowid) FROM campaign_recipients
|
||||
WHERE lure_code IS NOT NULL
|
||||
GROUP BY lure_code
|
||||
)`).Error; dedupErr != nil {
|
||||
return dedupErr
|
||||
}
|
||||
return createIndex()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ type CampaignTemplate struct {
|
||||
|
||||
URLPath string `gorm:"not null;default:'';index"`
|
||||
|
||||
// LureURLMode selects how a delivered lure URL carries the recipient
|
||||
// identifier. "query" appends ?id=<uuid>, "path" appends /<code> after
|
||||
// URLPath. The resolver accepts both forms regardless of this setting, so
|
||||
// changing it never breaks links that are already out.
|
||||
LureURLMode string `gorm:"not null;default:'query'"`
|
||||
// LureCodeAlgo names the generator that allocates codes for campaigns
|
||||
// created from this template.
|
||||
LureCodeAlgo string `gorm:"not null;default:'crockford32'"`
|
||||
// LureCodeLength is the number of characters in a generated code. Shorter
|
||||
// codes are easier to read aloud and easier to guess.
|
||||
LureCodeLength int `gorm:"not null;default:12"`
|
||||
|
||||
// IsUsable indicates if a template is usable based on if it has all the required
|
||||
// data such as domainID, landingPage and etc to be used in a campaign
|
||||
IsUsable bool `gorm:"not null;default:false;index"`
|
||||
|
||||
Reference in New Issue
Block a user