diff --git a/backend/acme/certmagic.go b/backend/acme/certmagic.go index 6685987..9eeef12 100644 --- a/backend/acme/certmagic.go +++ b/backend/acme/certmagic.go @@ -61,7 +61,9 @@ func setupCertMagic( if conf.TLSAuto() && conf.TLSHost() == name { return nil } - // check phishing host with managed TLS + // allow on-demand ACME only for domains that use managed TLS (let's encrypt / acme). + // own_managed_tls and self_signed_tls domains must never trigger ACME acquisition — + // their certificates are provided manually or generated internally. res := db. Select("id"). Where("name = ?", name). diff --git a/backend/model/proxy.go b/backend/model/proxy.go index 50e520e..24c2a59 100644 --- a/backend/model/proxy.go +++ b/backend/model/proxy.go @@ -20,6 +20,10 @@ type Proxy struct { Description nullable.Nullable[vo.OptionalString1024] `json:"description"` StartURL nullable.Nullable[vo.String1024] `json:"startURL"` ProxyConfig nullable.Nullable[vo.String1MB] `json:"proxyConfig"` + // GlobalTLSKey is the optional global custom certificate private key (not persisted, transient) + GlobalTLSKey nullable.Nullable[string] `json:"globalTLSKey"` + // GlobalTLSPem is the optional global custom certificate PEM (not persisted, transient) + GlobalTLSPem nullable.Nullable[string] `json:"globalTLSPem"` Company *Company `json:"-"` } diff --git a/backend/service/domain.go b/backend/service/domain.go index 65e7f7b..64322ca 100644 --- a/backend/service/domain.go +++ b/backend/service/domain.go @@ -903,6 +903,19 @@ func (d *Domain) handleOwnManagedTLS( ) return nil, errs.Wrap(err) } + // evict any previously cached certs for this domain so the stale cert is not served + existingCerts := d.CertMagicCache.AllMatchingCertificates(name) + if len(existingCerts) > 0 { + hashes := make([]string, 0, len(existingCerts)) + for _, c := range existingCerts { + hashes = append(hashes, c.Hash()) + } + d.CertMagicCache.Remove(hashes) + d.Logger.Debugw("evicted stale cached certs before replacing", + "domain", name, + "count", len(hashes), + ) + } // Create fresh buffers for caching since upload consumed the original buffers keyBufferForCache := bytes.NewBufferString(key) pemBufferForCache := bytes.NewBufferString(pem) @@ -910,7 +923,7 @@ func (d *Domain) handleOwnManagedTLS( ctx, pemBufferForCache.Bytes(), keyBufferForCache.Bytes(), - []string{name}, + []string{}, ) if err != nil { d.Logger.Errorw( @@ -1033,7 +1046,7 @@ func (d *Domain) handleSelfSignedTLS( ctx, pemBytes, keyBytes, - []string{name}, + []string{}, ) if err != nil { d.Logger.Errorw( diff --git a/backend/service/proxy.go b/backend/service/proxy.go index 7497672..bab2b2e 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -68,6 +68,7 @@ type ProxyServiceRules struct { // TLS modes: // - "managed": Use Let's Encrypt for automatic certificate management (DEFAULT) // - "self-signed": Use automatically generated self-signed certificates +// - "custom": Use a manually supplied certificate (key + pem) // // Configuration can be set globally and overridden per-host: // @@ -79,7 +80,7 @@ type ProxyServiceRules struct { // tls: // mode: "self-signed" # override global setting type ProxyServiceTLSConfig struct { - Mode string `yaml:"mode"` // "managed" | "self-signed" + Mode string `yaml:"mode"` // "managed" | "self-signed" | "custom" } // ProxyServiceImpersonateConfig represents client impersonation configuration @@ -756,6 +757,13 @@ func (m *Proxy) UpdateByID( if v, err := proxy.ProxyConfig.Get(); err == nil { current.ProxyConfig.Set(v) } + // copy transient cert fields — not persisted to db, but needed by syncProxyDomains + if v, err := proxy.GlobalTLSKey.Get(); err == nil { + current.GlobalTLSKey.Set(v) + } + if v, err := proxy.GlobalTLSPem.Get(); err == nil { + current.GlobalTLSPem.Set(v) + } // validate updated Proxy configuration if err := m.validateProxyConfigForUpdate(ctx, current, id); err != nil { @@ -890,6 +898,11 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P ) } + // validate domain-specific TLS config + if err := m.validateTLSConfig(domainConfig.TLS); err != nil { + return err + } + // validate domain-specific access control if err := m.validateAccessControl(domainConfig.Access); err != nil { return err @@ -911,6 +924,9 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P // validate global rules if config.Global != nil { + if err := m.validateTLSConfig(config.Global.TLS); err != nil { + return err + } if err := m.validateAccessControl(config.Global.Access); err != nil { return err } @@ -930,6 +946,52 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P } } + // validate custom TLS domains have a cert — either a new one is supplied or the domain already has one + newCertProvided := false + if globalKey, keyErr := proxy.GlobalTLSKey.Get(); keyErr == nil && len(globalKey) > 0 { + if globalPem, pemErr := proxy.GlobalTLSPem.Get(); pemErr == nil && len(globalPem) > 0 { + newCertProvided = true + } + } + if !newCertProvided { + // resolve the effective TLS mode for each host and check if any require a cert we don't have + for _, domainConfig := range config.Hosts { + if domainConfig == nil { + continue + } + // determine effective TLS mode for this host + effectiveTLS := "" + if domainConfig.TLS != nil && domainConfig.TLS.Mode != "" { + effectiveTLS = domainConfig.TLS.Mode + } else if config.Global != nil && config.Global.TLS != nil { + effectiveTLS = config.Global.TLS.Mode + } + if effectiveTLS != "custom" { + continue + } + // check if the phishing domain already has a custom cert in the db + phishingDomain, err := vo.NewString255(domainConfig.To) + if err != nil { + continue + } + existingDomain, err := m.DomainRepository.GetByName(ctx, phishingDomain, &repository.DomainOption{}) + if err != nil || existingDomain == nil { + // new domain — no existing cert possible + return validate.WrapErrorWithField( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + "proxyConfig", + ) + } + if !existingDomain.OwnManagedTLS.MustGet() { + // existing domain but no custom cert yet + return validate.WrapErrorWithField( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + "proxyConfig", + ) + } + } + } + return nil } @@ -1546,9 +1608,9 @@ func (m *Proxy) validateTLSConfig(tlsConfig *ProxyServiceTLSConfig) error { } // validate TLS mode - if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" { + if tlsConfig.Mode != "" && tlsConfig.Mode != "managed" && tlsConfig.Mode != "self-signed" && tlsConfig.Mode != "custom" { return validate.WrapErrorWithField( - errors.New("tls.mode must be either 'managed' or 'self-signed'"), + errors.New("tls.mode must be either 'managed', 'self-signed', or 'custom'"), "proxyConfig", ) } @@ -2399,6 +2461,28 @@ func (m *Proxy) createProxyDomains(ctx context.Context, session *model.Session, domain.ManagedTLS.Set(false) domain.OwnManagedTLS.Set(false) domain.SelfSignedTLS.Set(true) + } else if tlsMode == "custom" { + // check if a global cert is provided on the proxy model + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the global cert to this domain + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(true) + domain.SelfSignedTLS.Set(false) + domain.OwnManagedTLSKey.Set(globalKey) + domain.OwnManagedTLSPem.Set(globalPem) + } else { + // no cert provided for a new domain — cannot configure custom TLS without a certificate + m.Logger.Errorw("cannot create domain with custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", domainConfig.To, + ) + m.rollbackCreatedDomains(ctx, session, createdDomains) + return errs.NewValidationError( + fmt.Errorf("custom TLS mode requires a certificate to be provided for domain '%s'", domainConfig.To), + ) + } } else { // default to managed domain.ManagedTLS.Set(true) @@ -2661,18 +2745,44 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr // check current TLS settings currentManagedTLS := existingDomain.ManagedTLS.MustGet() currentSelfSignedTLS := existingDomain.SelfSignedTLS.MustGet() + currentOwnManagedTLS := existingDomain.OwnManagedTLS.MustGet() // determine if TLS settings need updating if tlsMode == "self-signed" && !currentSelfSignedTLS { + // switching to self-signed — clear any existing custom cert flag so updateDomain cleans it up existingDomain.ManagedTLS.Set(false) existingDomain.OwnManagedTLS.Set(false) existingDomain.SelfSignedTLS.Set(true) needsUpdate = true } else if tlsMode == "managed" && !currentManagedTLS { + // switching to managed — clear any existing custom cert flag so updateDomain cleans it up existingDomain.ManagedTLS.Set(true) existingDomain.OwnManagedTLS.Set(false) existingDomain.SelfSignedTLS.Set(false) needsUpdate = true + } else if tlsMode == "custom" { + // check if a new global cert is being pushed + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the new global cert — always update when a cert is explicitly provided + existingDomain.ManagedTLS.Set(false) + existingDomain.OwnManagedTLS.Set(true) + existingDomain.SelfSignedTLS.Set(false) + existingDomain.OwnManagedTLSKey.Set(globalKey) + existingDomain.OwnManagedTLSPem.Set(globalPem) + needsUpdate = true + } else if !currentOwnManagedTLS { + // no new cert provided and domain doesn't already have a custom cert — cannot switch to custom + m.Logger.Warnw("cannot switch domain to custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", phishingDomain, + ) + syncErrors = append(syncErrors, fmt.Sprintf("cannot switch domain '%s' to custom TLS without providing a certificate", phishingDomain)) + errorCount++ + continue + } + // if currentOwnManagedTLS is true and no new cert provided — preserve existing cert, no update needed } if needsUpdate { @@ -2735,6 +2845,27 @@ func (m *Proxy) syncProxyDomains(ctx context.Context, session *model.Session, pr domain.ManagedTLS.Set(false) domain.OwnManagedTLS.Set(false) domain.SelfSignedTLS.Set(true) + } else if tlsMode == "custom" { + // check if a global cert is provided on the proxy model + globalKey, keyErr := proxy.GlobalTLSKey.Get() + globalPem, pemErr := proxy.GlobalTLSPem.Get() + if keyErr == nil && pemErr == nil && len(globalKey) > 0 && len(globalPem) > 0 { + // apply the global cert to this domain + domain.ManagedTLS.Set(false) + domain.OwnManagedTLS.Set(true) + domain.SelfSignedTLS.Set(false) + domain.OwnManagedTLSKey.Set(globalKey) + domain.OwnManagedTLSPem.Set(globalPem) + } else { + // no cert provided for a new domain — cannot configure custom TLS without a certificate + m.Logger.Warnw("cannot add domain with custom TLS without a certificate", + "proxyID", proxyID.String(), + "domain", phishingDomain, + ) + syncErrors = append(syncErrors, fmt.Sprintf("cannot add domain '%s' with custom TLS without providing a certificate", phishingDomain)) + errorCount++ + continue + } } else { // default to managed domain.ManagedTLS.Set(true) diff --git a/backend/testfiles/certs/cert.test/cert.key b/backend/testfiles/certs/cert.test/cert.key new file mode 100644 index 0000000..597a7af --- /dev/null +++ b/backend/testfiles/certs/cert.test/cert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCixyMt5qK2ulK3 +t7dS1BkNaxbuZNexYLCmWSWUN9tdboAKIapLPcV3/jwqZwP04kaU/ObUp+1vXjK9 +2dJXASZQ5HuZO3438oCsgeKGe8FRsbuK/2gfXbWrLxDhHW7hTlwhuuC/SRe2u+n1 +Qjrz67iQQ1QdfdE3HhLzllU9SFPcIW+l1T7oILJt0NLdEbjBoecxP1tE7DDw7E87 +y0ipPDBz5Q9ZwS95vaJG013MvM7Ly5gg6+4E108JKw5Rl0NoUZVhi1bNJcrg1n2K +R7aqX+URtcA8sTMEKqXzTGUvXxvGsXESwlttb5tHjwKaRxLRBqSi0IEoad0h+Vlx +o/z+IPQdAgMBAAECggEADBEbtdCvbQ2779wB9MIKlDJe0hzBc2gTc4l2ETPg1av8 +eIPNX+PvSkY1B8ze/+pxFaT320Z4zs6aj8HHGrI+bAG8FEk5dQR+1T4qekWDUiVZ +6n0+wHbnodrzDK64/dSbkAtbLNYwy75h1hyEsHj5eh+CBU7cUXjJXXheaK5+ocaH +swmkXMigidMkfzAURhwhJ4ce6A3F9wVe8+SJlOHCx5m5usIXthg9gTYgq6EymALt +C1zRlO2xteB6qzvHu1FgDKWkHmX1PgiKr1L3gBNjJutuY8TgnbiaQ9pRMlaeE850 +ctxGL8Hg11llA6sBGdUuUEA2UlUMJEIadJjVqcEjKwKBgQDN+OyGiOimY+bjjBW6 +NLq8/f/T2T549DGsSNt+EiGj+gJlif1jZqVtgtjRta/kbwF5f6Zx3Q9I4J69iw8c +wvmS1ip4UXhrJKua+Wvk6mEeTAejazH46paOgX97TUfmsSQC2+R2iM8xKNxbpxCD +k4wNPt1rNeTCA/SBF3dKDlqrPwKBgQDKUHDM+/YIGuXa8DAe34E/Tl2Onq/R9yA4 +WNXnjKsOk/aK/fshzVIS7wcGpXzK74i9ds1z19ojUgYI4qV7XcZ9DiXewqP8tXtI +llNU8DXUOBrFX65d//1XiwU5b8bplXwiOB1Jd5hue0HPscsaic0EYS/j2ByXD+0l +8XJ3j/ZVowKBgQCwxBSZUR34znv0hOCQsXghggrwEN0giNGofc6BX6YnSASOh+JC +UHFgjo7tSvPtI6csUnTR+1mGvd795D3P/TSa49oG8ERcD1iG48/I4az/h1h20yRL +72fOXSy+8Q/n19aD7Zsgb0EBe4PB1JrDkPj81RrJS7NLHoHT2AO0NqVxmQKBgDtw +ApPWemPLMzhtVFXdqCUnKslZyaHQDsE/KCjM5Px1b/tJvtwhbDlvzAqh19XvJac0 +HgwooEe8M1Ws8J0b4dKfs3SMjo0R7FRZBcZwhAADM6pE//9R0+ZCS5iiRDgf2MZc +4g3RexEKWT1hqJ/1WCwvOVihB1VCMpPxKYYC34YtAoGAexR7JhjgjztmP65HzyCv +hff37H89MomRWUyaAlQ8Sqpj9bTMfcpsAmZ2qMGWKgUCfwQc1n3jRuSrWhotGWKZ +1n3xQ5q3IgBIOWBJHJ1Ez4HF9F0xVrrwPjYH/OoFBPQKU6QGkIr68YsQq+Yc+0BF +ReemAd9XD2vSRrBh1/WJKaI= +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/cert.test/cert.pem b/backend/testfiles/certs/cert.test/cert.pem new file mode 100644 index 0000000..fbe8135 --- /dev/null +++ b/backend/testfiles/certs/cert.test/cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgIUNv0gRyo+x0RDbBFBxmP/T3CfVowwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJY2VydC50ZXN0MB4XDTI2MDQxOTEzMDQyOFoXDTM2MDQx +NjEzMDQyOFowFDESMBAGA1UEAwwJY2VydC50ZXN0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAoscjLeaitrpSt7e3UtQZDWsW7mTXsWCwplkllDfbXW6A +CiGqSz3Fd/48KmcD9OJGlPzm1Kftb14yvdnSVwEmUOR7mTt+N/KArIHihnvBUbG7 +iv9oH121qy8Q4R1u4U5cIbrgv0kXtrvp9UI68+u4kENUHX3RNx4S85ZVPUhT3CFv +pdU+6CCybdDS3RG4waHnMT9bROww8OxPO8tIqTwwc+UPWcEveb2iRtNdzLzOy8uY +IOvuBNdPCSsOUZdDaFGVYYtWzSXK4NZ9ike2ql/lEbXAPLEzBCql80xlL18bxrFx +EsJbbW+bR48CmkcS0QakotCBKGndIflZcaP8/iD0HQIDAQABo2kwZzAdBgNVHQ4E +FgQUWtHAW8LyyJpevjePLzgRSQBTP9wwHwYDVR0jBBgwFoAUWtHAW8LyyJpevjeP +LzgRSQBTP9wwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTALggljZXJ0LnRlc3Qw +DQYJKoZIhvcNAQELBQADggEBADf3QqqNrG97rxTe+QmCltKRD/4Rj9tekoXKPi/c +E1343ZwfH2WWnhZFQnZnWeck8BhXqEjvxmRP7IHOj+WyeE0l+rANR3YvmqkSrqs3 +N0JF47OfabyNUYJIO5wKochCUqHiM1yyET3RD4Mcz435SNRCdk6G9ytLt+19zrRx +7h+vvh7Bmw3mhCgcPwdBcmtAwoVlVmdwzwpVZ7GBpr9SlA7WxQo4HYEd66XJK7Pk +p58qWkDHHBPIOO4DvuuvBwU4VMl4TynDecAK6qbIDghUaSDeWDHhN8rpLsuOjYwI +6ZzoKb4FiZjYn8QhKcTSJjf082s66ovfml0JglZp6AScYq8= +-----END CERTIFICATE----- diff --git a/backend/testfiles/certs/mycert.test/mycert.test.crt b/backend/testfiles/certs/mycert.test/mycert.test.crt new file mode 100644 index 0000000..07f2768 --- /dev/null +++ b/backend/testfiles/certs/mycert.test/mycert.test.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEszCCApsCFD5xluzzeuIZbCYcshr+3OfR3e0eMA0GCSqGSIb3DQEBDQUAMBYx +FDASBgNVBAMMC215Y2VydC50ZXN0MB4XDTI2MDQxNjIxMDg1MloXDTM2MDQxMzIx +MDg1MlowFjEUMBIGA1UEAwwLbXljZXJ0LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC0qRS0tUDPwLVNEuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZ +u0mDI21s/GU1lHER+S4f13Df1Dvp7bSl/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC +6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ34APVp8nptA+NSwwk9upiJzbUfMb8yxBQ +YaT9Q194J5LlQK5Rge9bJEDrATSTqOVBq6227yQso1nr5nEpbwHfDmU44py/OLKf +byYvjEI9PGUBJE6rjC5iS23RKHh/mBSeGLuEF8XnXqDV2EfWkozpTBri0RdTVbNP +aYIpgueYsMTGoRXJ/I0amGaju0uqGbqbObkqFeSZOyPg3zssmqQv7bMXSzJjt10X +ZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjVZViigJbnbm1KFuKpBUq5WFN0MrViVsWL +ACwETw1MjA8bLjHfcwSG/8dB3vnHLgulvWZut0g6nJaX2fPxmEPbSboh9wbdB5o4 +iCcYx1X3bc0cx6r80caCnICvLjYcf+i89TUPpXW7UqlZngjoohIfhhb6fKPWxT6v +w4dG2SUtRhoRzLP9GWbawKnlrEuctVTbAYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ7 +6bQRa6LT9Ddmq1QHXgvVn81+d2pJDOefTVDAacWij6wIrpjWg/z8qx1kFJPW5wID +AQABMA0GCSqGSIb3DQEBDQUAA4ICAQCVRRmMY8zliozIA4s5O2dYXaPM6wnGErmx +QzEryE6hQT6gIkPrJExHefokrKAMZy1+waslugIYbtURDzqu9O+zJaf8WOzhYSxL ++3NLFemfBVvP68c3g6y2Hy3gSZ2ZZb4yeiG8aHB74rl5yxaUmdJ690zDBaJ7NC5G +ZS84iSWZMCjAcqViLqQ1RvxybcAduW5vPVPej4U3HIdUQ6YvOo+7x+BNW5Ost4xv +ytR1IZBRdxdmXQEOp049EENzqLPq9hvwoUgKZqj9QZT5CEO985Cvo0U9BPpsGWg6 +YcecWXues/TmacJA55rCdJ279wiQKTmaLvI1D3vpdbYBdpAwH6l4eMmb7XOppa8s +50hJpPoxtQ5aqU6Twyp0mhf2/ie70oRoS0UqrLMlR/dArw3qDZqYt7p/frsIHMgu +dhbwdFhczXTQTl+Fz+k96nFYN8r9CYZrxzOi2fZCUdN42MqAfSeeqm6EwfCOHM6G +Mhccf3ZfnSKLXkfL/jiyPtZRmNaO+zAJSjP7VKMyeqk8M4XYQnWBci1JrWcBy7mV +dD9WUb51yUKmRIggOKzNuXp9splFP9yZTUFhtz4ztU6HExTwnADQRQFB7crLaoTU +CuhJo3vxZGds7FDcw3/NYRxWsjXcKwcE3nBewVSfdJCZBkcA4KztuCuqTB23rO6E +Ew+AoWp2AQ== +-----END CERTIFICATE----- diff --git a/backend/testfiles/certs/mycert.test/mycert.test.key b/backend/testfiles/certs/mycert.test/mycert.test.key new file mode 100644 index 0000000..188d70c --- /dev/null +++ b/backend/testfiles/certs/mycert.test/mycert.test.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +4lF4+z9bxHS9QBHIkTOjBYGeVGkSpQ== +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/mycert.test/yourdomain.pem b/backend/testfiles/certs/mycert.test/yourdomain.pem new file mode 100644 index 0000000..55cdf54 --- /dev/null +++ b/backend/testfiles/certs/mycert.test/yourdomain.pem @@ -0,0 +1,80 @@ +-----BEGIN CERTIFICATE----- +MIIEszCCApsCFD5xluzzeuIZbCYcshr+3OfR3e0eMA0GCSqGSIb3DQEBDQUAMBYx +FDASBgNVBAMMC215Y2VydC50ZXN0MB4XDTI2MDQxNjIxMDg1MloXDTM2MDQxMzIx +MDg1MlowFjEUMBIGA1UEAwwLbXljZXJ0LnRlc3QwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC0qRS0tUDPwLVNEuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZ +u0mDI21s/GU1lHER+S4f13Df1Dvp7bSl/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC +6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ34APVp8nptA+NSwwk9upiJzbUfMb8yxBQ +YaT9Q194J5LlQK5Rge9bJEDrATSTqOVBq6227yQso1nr5nEpbwHfDmU44py/OLKf +byYvjEI9PGUBJE6rjC5iS23RKHh/mBSeGLuEF8XnXqDV2EfWkozpTBri0RdTVbNP +aYIpgueYsMTGoRXJ/I0amGaju0uqGbqbObkqFeSZOyPg3zssmqQv7bMXSzJjt10X +ZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjVZViigJbnbm1KFuKpBUq5WFN0MrViVsWL +ACwETw1MjA8bLjHfcwSG/8dB3vnHLgulvWZut0g6nJaX2fPxmEPbSboh9wbdB5o4 +iCcYx1X3bc0cx6r80caCnICvLjYcf+i89TUPpXW7UqlZngjoohIfhhb6fKPWxT6v +w4dG2SUtRhoRzLP9GWbawKnlrEuctVTbAYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ7 +6bQRa6LT9Ddmq1QHXgvVn81+d2pJDOefTVDAacWij6wIrpjWg/z8qx1kFJPW5wID +AQABMA0GCSqGSIb3DQEBDQUAA4ICAQCVRRmMY8zliozIA4s5O2dYXaPM6wnGErmx +QzEryE6hQT6gIkPrJExHefokrKAMZy1+waslugIYbtURDzqu9O+zJaf8WOzhYSxL ++3NLFemfBVvP68c3g6y2Hy3gSZ2ZZb4yeiG8aHB74rl5yxaUmdJ690zDBaJ7NC5G +ZS84iSWZMCjAcqViLqQ1RvxybcAduW5vPVPej4U3HIdUQ6YvOo+7x+BNW5Ost4xv +ytR1IZBRdxdmXQEOp049EENzqLPq9hvwoUgKZqj9QZT5CEO985Cvo0U9BPpsGWg6 +YcecWXues/TmacJA55rCdJ279wiQKTmaLvI1D3vpdbYBdpAwH6l4eMmb7XOppa8s +50hJpPoxtQ5aqU6Twyp0mhf2/ie70oRoS0UqrLMlR/dArw3qDZqYt7p/frsIHMgu +dhbwdFhczXTQTl+Fz+k96nFYN8r9CYZrxzOi2fZCUdN42MqAfSeeqm6EwfCOHM6G +Mhccf3ZfnSKLXkfL/jiyPtZRmNaO+zAJSjP7VKMyeqk8M4XYQnWBci1JrWcBy7mV +dD9WUb51yUKmRIggOKzNuXp9splFP9yZTUFhtz4ztU6HExTwnADQRQFB7crLaoTU +CuhJo3vxZGds7FDcw3/NYRxWsjXcKwcE3nBewVSfdJCZBkcA4KztuCuqTB23rO6E +Ew+AoWp2AQ== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQC0qRS0tUDPwLVN +EuB6j8ADS9RQQIZb7jUULHzpi9t1z8QZu0mDI21s/GU1lHER+S4f13Df1Dvp7bSl +/nR6Pkq9PYMR+h2ExHqHhQII4kY2cxaC6fZdIeyxIGaYnlIRH05fqG8NbKGhhAJ3 +4APVp8nptA+NSwwk9upiJzbUfMb8yxBQYaT9Q194J5LlQK5Rge9bJEDrATSTqOVB +q6227yQso1nr5nEpbwHfDmU44py/OLKfbyYvjEI9PGUBJE6rjC5iS23RKHh/mBSe +GLuEF8XnXqDV2EfWkozpTBri0RdTVbNPaYIpgueYsMTGoRXJ/I0amGaju0uqGbqb +ObkqFeSZOyPg3zssmqQv7bMXSzJjt10XZcXKu62Ba064o8gKJ4wQ/lg5aJp1ysjV +ZViigJbnbm1KFuKpBUq5WFN0MrViVsWLACwETw1MjA8bLjHfcwSG/8dB3vnHLgul +vWZut0g6nJaX2fPxmEPbSboh9wbdB5o4iCcYx1X3bc0cx6r80caCnICvLjYcf+i8 +9TUPpXW7UqlZngjoohIfhhb6fKPWxT6vw4dG2SUtRhoRzLP9GWbawKnlrEuctVTb +AYnN6BtluqGX1DfEMp0j+wZl6o2YMyQ76bQRa6LT9Ddmq1QHXgvVn81+d2pJDOef +TVDAacWij6wIrpjWg/z8qx1kFJPW5wIDAQABAoICABxEEhsd+s3RMxdOhfRsdliY +WNfU6KYMifMl7MX7swgRTEzRr/RGgDyblthAaUUsd/9/t/Iv+iR6eRe5orls8p5T +jU+Bkx8ZQKngcSDjtPmYSHmOJ+o1axuMMWaH6tjb+D62PeiF9RoDa1bHJBfIgKps +mOb8oCH5HKiOcHaO2Ua8ch5+0I+BpuJyn/n3hJlNxkiRI7PBL1vlAo2jp0fRIxdK +2EBfq0KWKDRL7nbChKTUjE9ZVspSBxvJTcJVJGvpwGjHsB8YARZxkdQ/PrjcTANh +xlypp2pr6S59UP2Tml3YXE2R+U1hD7b6UqdYlRinx4oANfBAgduo2o372jvlS1aR +oRv4UTw+KNaoN9Pj4aiMfDIqef9PlxX2sxBdhKY8YMWvfpf0RubR01TuWJD8w8sR +7bP3iFNHHQRNZBzrA+LaBJcAOe4QxPWc0rhQ097nkhlISOCQo2RXgSGr20S9TZLE +gsVI02r2P6kxMGxy7LFJq5qUt5ACC07e6LjJVwXTgdg7vNOeeKC7nr8AytuU/JmF +aDcxdkXtOAqo++0jwEj/1Q4Q08jnj36ChFfSPmb6AkJF4kmPSUyG2BxQYOsKtC6s +WFRwm4TrTpyVDUYdSOb1joYFiFbamIXk/p4/+GZKpk9KeAGdlq4Bx4NztUvpieLb +LMI/CTjTwnHQfZKS1MUNAoIBAQD1gIiCsO7HNN9yTCc1L0baJnvXOq29/GAkIcyp +Bh4Kw9WEziSK0BiYHM5Baie4NPZsHNHDgHIn9XIJw1N1qrMOt8Xw1McEtnv3Ne+Q +4VCsrfTldcMKDQLNMHDh3geghn9jcgf+ZcwOWvAe6F2Ie/AkufQPdHjPV88xTRqu +iheaYKR+gWN2bfxdK2dD8FreJrTVZ+ck8gdCkTNFt4XwBKY3hY/CL990E0cVmGRZ +y2WRx4/qavbflVawVbNcN4b1yJQIDWmaWDa4kzkjHSIdELfWtIoICEK3lobcixFE +dIR/jKseRr3pi0N0gQYonaL0QcLTpP5Zio+NQmkxjJXxM7jNAoIBAQC8Yr0Amtrl +QSomKdjJh+sdf+AvTjR7aHKaSqrgvxczChVceuDaNQ8lz4HDDd3LfPAyPiW7PX5g +ujgjVPRKhr3DqLsVCOm199VichKlpQRYuDFISAuYaW/KgYGv+OtLxv5I4QIhSQDx +O93LEiOvo4H1iGAxUuxs3mGi/hqSZxve+LAzv+wm7OiCQcT4Z6l+Vxj7FXuzscLs +N+28fqS0brP8UXakIjHpn196wXl82zAWoalH3rfmsWSjF5npm09D/84gcE0T3xcQ +5p0YJysya/9Nu0pvwOTv+W5Y3BvZJOcPzGaTgMQ3uuwiiNRbmkVHu2W9SzND8kOp +Dl19t49CRV6DAoIBAGvtHJYvyFkE8nJh7h6gcQp4Ppso7baG25Em1r07tjtPSm++ +3Cu2PgmpKDdzvpBpoCd5J/JFZmoQqhiGqQsihuMigT9Vm0SEIM1WBcJwezHeq7mw +YpTpkWC5Ofbh0AKO/jOurrr075cj/UnpJy1YJwNOSG/+6Rll5e0rk15F0QiKEeaX +ZS1sPrSK3zPr11awN3FV4zTHvc9S2/J7MsOIl7Xy3nck6pwx2V8yBnO/SiCjVa5d +Zbh3A4wzsM0KkCc/DWzY0KMMwsmz1zuLlDKo5djat4++ae4hm5oa/PVWL+WO5q9B +tD2WfooaKqXyXu/4dPjsIPEmS+Ny3aHtxwEplsUCggEAOqU6VV/f2RKqPms0k7h+ +VxaiAdgEuo5PbvzjqUeTv03aTInsScHOz2SD7ub4Lwrb86gpMtr35sDSDR27VyAP +H0P9yZSWvRFEGnuMloiCi+P7Y5caFP5t0Mr0RoXlKhfuvV1evmHtqyuJ5lflSB5M +rNUhrPk1pMat+oHEX+M9Z/JfWBzdNVj3IOW8neAXgb83haKwecZS+hqHJfD+8TSt +T1VE69/BTgtRO/PTEC1kEQeOnVMWSPjcbXFBdtnkmTSfRLXxKMiAc8B3EzfOWMoK +FnbBu3x/SL2Lvpn3CWhVjjOBk1W4v+iu7ilOgp3KB4StLXqloPdgXNaeAC8OqADU +ZQKCAQEAxA+PVqv0tTud0O1MwEJaT+2QvC869zTxPC1bmB98FtrpiTKFtjH08xgz +fRADaYiQLavpGgPM5r8vVZY1EHDxrx+R2Xa1BmYpedwcz8ryl2lGg9RLR2YEVRX3 +dNkFu6P5CTawsd251jZWLBZOMKPvVqSb95ORgfSAscciJF2WSMdYKWixNLT4KAta +RDv6ArAV0l3caBB8RLlkhkHAvOBjvmc4vPcm/eZ6Y5UNRbP3Uqc1Ea+ILYfOWWQV +VYk4CZ0wvOhCIg9joJDsv/NVdt+LcjLPz7yUAW8BTLUUV03XCnx3w9HM0FqZfvIf +4lF4+z9bxHS9QBHIkTOjBYGeVGkSpQ== +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/wildcard/cert.key b/backend/testfiles/certs/wildcard/cert.key new file mode 100644 index 0000000..5d575f8 --- /dev/null +++ b/backend/testfiles/certs/wildcard/cert.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC0kJclAABh6Ocf +QcCj9+Dh6fUSyefnfqcVG3CYa0F/TENIZLAdQXGSXfprkQnhUQPfPfSOGiJ27Vme +sQiUTfLidqRgFXEF4JQgR6oO5au6llBBKU79fkw9UJ4OR2AtE136gGx9aUF4iLzT +UU019KNMnY7ICnAnd8dL69hY6pg9rCGiZmiiWAZ4XkxNkN3L5GMHMDNbcEWMCalp +6rP75rAvlXlhOUKomEZN1zLMu3FSWuMMCIbc2Anaq5Z2NYmf61YPb48/lklo5Klz +s0dN1ktBv7QMybOIP0YZnPNa76SDZ5bi1YYaCQybs6l845vfzEseAew+23RXXkam +QLSw2ZldAgMBAAECggEABk9rSNAS3gPCrVVMmIOy0z0BjeeKeDef5TKb9mer1qQL +/JKwPkV+5OuYiHvSyZIIPZwFA5ZRkAXEH7H2J8tVlVPUyMWBuVUw4rPpHd4pkzq/ +kUISpZ9CpiiUGCb8ayGFzkRWBkf7EW2jPmCqqcZj5+s1sIiqL2brNccHPOGYnRxr +eN5P+JGNN3Gn+LRy26xGgdaRdGDuWHQJOStQUFQvmLswjPui5hhNtJVvUmvQ/XlP +peWSL3mV/07fhBv7PcBxyeUJx+zk8NHSSNDXKT5fkGKAS4FAqtQziXKqqn3G8nKk +8jaBPaJli6Ezw6NLlUrUax0DbmQYopHLQgMm6vJ2zQKBgQDpHgKKNxlqha3xf8Y0 +NCD4T3Ju3cF2T/U/lDqRwZo97wZVnwHIfiG19pfhHbEshYvolUb3Frnro1ba60ed +9ePGGATxi7zPETkh15fZF2TJcW1GMBYeD5vTBkkyhdTnPFOBEcArVBQgTUm/XRaR +AZRefXM73siXIwdYiKHtIIdPHwKBgQDGSf5bzjfQoaEudrW8K8ZNiDEMZ6hR96so +iSMh7q73AwhiEnMacVezAN4WDLqHH8knCd0WC/b7R6ocRDuG8sFOzS9peIiklnn3 +A768LzFex+6AantOE2LEE5Cx1kjQ2aj2KdGkrXtBZpXMOVDpRSdNZIYPCj8pNgFn +rE9abILUAwKBgQCyV60lxIWDQwYSDei6o27dyRoIy0pokz9TBrnQLMctvqGf+2fH +1QdBSIhlRuv23axtoVaLTi2qommeTgWaSTWapWGS0Y7+83Q7+c5H3WfT3Rz2Z29k +TBiwVszFBDIfPb28rrHP9CD5nWdgKX1MLmMt7ter5AKd7cR+7PjEivA5jQKBgQCC +PXuqhUq36FHcGPDJhd8cccX1pegy3oA3gcvnr8SQThelgwTDa4r08i7tQLMLqd8P +mzTyFC3HYozjQBXxT2WVAsSPfDIUGRpHGtie9khxPtTy1/3hjG4k58z0YhE1zKFj +/pfKmIAKtvzRRRxV+6wS82HyYwKVaPmHRPBiLj/ITQKBgAOG6wa2ZMClX9MtNqMD +w0voX4xo+x+4l4v8GgtLQns1wyhW8BG8CYEzogI/Dx5jT9Q787F+gRctjzQPHvm1 +a4E7MwKDgklAJFaOwNmX7DEqkAmfszLjRZFTleOK594/FEjbWTi5CWqW55pZ/C3S +61VYVaqIn84aGCbIaLI5vULr +-----END PRIVATE KEY----- diff --git a/backend/testfiles/certs/wildcard/cert.pem b/backend/testfiles/certs/wildcard/cert.pem new file mode 100644 index 0000000..58e9acc --- /dev/null +++ b/backend/testfiles/certs/wildcard/cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQDCCAiigAwIBAgIUAp5wJoElDOa8IzoIcfUj917Y0MMwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPKi53aWxkY2FyZC50ZXN0MB4XDTI2MDQxOTEzMTAwNloX +DTM2MDQxNjEzMTAwNlowGjEYMBYGA1UEAwwPKi53aWxkY2FyZC50ZXN0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtJCXJQAAYejnH0HAo/fg4en1Esnn +536nFRtwmGtBf0xDSGSwHUFxkl36a5EJ4VED3z30jhoidu1ZnrEIlE3y4nakYBVx +BeCUIEeqDuWrupZQQSlO/X5MPVCeDkdgLRNd+oBsfWlBeIi801FNNfSjTJ2OyApw +J3fHS+vYWOqYPawhomZoolgGeF5MTZDdy+RjBzAzW3BFjAmpaeqz++awL5V5YTlC +qJhGTdcyzLtxUlrjDAiG3NgJ2quWdjWJn+tWD2+PP5ZJaOSpc7NHTdZLQb+0DMmz +iD9GGZzzWu+kg2eW4tWGGgkMm7OpfOOb38xLHgHsPtt0V15GpkC0sNmZXQIDAQAB +o34wfDAdBgNVHQ4EFgQU62UOXUjmffiNy6BJm8Qv+3qKJq0wHwYDVR0jBBgwFoAU +62UOXUjmffiNy6BJm8Qv+3qKJq0wDwYDVR0TAQH/BAUwAwEB/zApBgNVHREEIjAg +gg8qLndpbGRjYXJkLnRlc3SCDXdpbGRjYXJkLnRlc3QwDQYJKoZIhvcNAQELBQAD +ggEBAFu8H73GimARkhT9kW/lgVxs8/cvBOjskdUOtmndhLi7p1ile1Md+E59tcm0 +lp7zMlCsOZGhU0eS70Y/rTL02sSS4pWPARxQr8KqjtqS2+3PBnBuyb8lwh00h1yA +G+YnF4fNom7XhBjXhxtKvajrrywyuxOTga4LQ9aqs4qc+ZCH30LYl0ev93bEEMf/ +MKjEenlfw4KDrxDmBJzMPG8T4npeoigz9olOQFWW1nd+4GfLIpENCzkIhiuvq2JA +KVGB3kj7OyECkNk9WA6eeUUu8QMFxv/G9j3BW8Uh6ENLHnEBd1CthSUXv0kTSwQ0 +ZLygXMZfBxrhaBO/TgUDOZmyYW8= +-----END CERTIFICATE----- diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 6493f49..a6cc8e4 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -3226,15 +3226,27 @@ export class API { * @param {string} proxy.startURL * @param {string} proxy.proxyConfig * @param {string} proxy.companyID + * @param {string} [proxy.globalTLSKey] + * @param {string} [proxy.globalTLSPem] * @returns {Promise} */ - create: async ({ name, description, startURL, proxyConfig, companyID }) => { + create: async ({ + name, + description, + startURL, + proxyConfig, + companyID, + globalTLSKey, + globalTLSPem + }) => { return await postJSON(this.getPath('/proxy'), { name: name, description: description, startURL: startURL, proxyConfig: proxyConfig, - companyID: companyID + companyID: companyID, + ...(globalTLSKey ? { globalTLSKey } : {}), + ...(globalTLSPem ? { globalTLSPem } : {}) }); }, @@ -3247,6 +3259,8 @@ export class API { * @param {string} proxy.description * @param {string} proxy.startURL * @param {string} proxy.proxyConfig + * @param {string} [proxy.globalTLSKey] + * @param {string} [proxy.globalTLSPem] * @returns {Promise} */ update: async (id, proxy) => { diff --git a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte index beaca17..9aedaa1 100644 --- a/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte +++ b/frontend/src/lib/components/proxy/ProxyConfigBuilder.svelte @@ -4,6 +4,7 @@ import TextFieldSelect from '$lib/components/TextFieldSelect.svelte'; import TextareaField from '$lib/components/TextareaField.svelte'; import Search from '$lib/components/Search.svelte'; + import FileField from '$lib/components/FileField.svelte'; import jsyaml, { dumpWithLiteralStrings } from '$lib/components/yaml/index.js'; export let config = null; @@ -302,6 +303,8 @@ hosts: [] }; expandedHostIndex = -1; + globalTLSKey = ''; + globalTLSPem = ''; } // helper to remove internal _id fields before serialization @@ -697,7 +700,8 @@ // options const tlsModes = [ { value: 'managed', label: 'Managed' }, - { value: 'self-signed', label: 'Self-signed' } + { value: 'self-signed', label: 'Self-signed' }, + { value: 'custom', label: 'Custom' } ]; const tlsModesWithEmpty = [ { value: '', label: '(Use global default)' }, @@ -1022,6 +1026,36 @@ // file input reference for import let fileInput = null; + // global cert for custom TLS mode (not part of yaml config, sent separately to parent) + let globalTLSKey = ''; + let globalTLSPem = ''; + + /** + * @param {Event} event + * @param {'globalTLSKey'|'globalTLSPem'} target + */ + function onGlobalCertFile(event, target) { + const file = /** @type {HTMLInputElement} */ (event.target).files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + if (target === 'globalTLSKey') { + globalTLSKey = e.target.result.toString(); + } else { + globalTLSPem = e.target.result.toString(); + } + dispatchCertChange(); + }; + reader.readAsText(file); + } + + function dispatchCertChange() { + dispatch('certChange', { + globalTLSKey, + globalTLSPem + }); + } + // export configuration to YAML file with metadata function exportConfig() { // build the config with _meta section @@ -1380,1416 +1414,402 @@
- {#if activeTab === 'basic'} - -
- - - -
-
-

General

-
-
-
- -
-
- -
-
- - Domain must match a phishing domain in the Hosts tab -
-
+ +
+ + + +
+
+

General

- -
-

Proxy Settings

-
-
- - Forward Proxy - - Route all traffic through this proxy -
+
+
+ +
+
+ +
+
+ + Domain must match a phishing domain in the Hosts tab
- {:else if activeTab === 'hosts'} -
- -
- - {#if configData.hosts.length > 3} -
- -
- {/if} -
- {#if configData.hosts.length > 0} -
- {#each filteredHosts as { host, index: i }} - - {/each} -
- {:else} -
-

No hosts configured

- -
- {/if} + +
+

Proxy Settings

+
+
+ + Forward Proxy + + Route all traffic through this proxy
- - -
- {#if expandedHostIndex >= 0 && configData.hosts[expandedHostIndex]} -
-
- {configData.hosts[expandedHostIndex].to || 'New Host'} - - {configData.hosts[expandedHostIndex].domain || 'target'} -
-
+
+
+
+ +
+ + {#if configData.hosts.length > 3} +
+ +
+ {/if} +
+ {#if configData.hosts.length > 0} +
+ {#each filteredHosts as { host, index: i }} - -
-
- - -
- - - - - -
- -
- {#if currentHostTab === 'settings'} -
-
- + From - Phishing Domain - - {#if hasError(`hosts.${expandedHostIndex}.to`)} - {getError(`hosts.${expandedHostIndex}.to`)} - {:else} - Your phishing domain that will serve the content - {/if} -
-
- {host.to || 'New Host'} - Target Domain - - {#if hasError(`hosts.${expandedHostIndex}.domain`)} - {getError(`hosts.${expandedHostIndex}.domain`)} - {:else} - The legitimate domain being impersonated - {/if}
-
- + To + {host.domain || '...'} - Scheme -
- {#if configData.hosts[expandedHostIndex].tls} -
- - TLS Mode - + {#if host.capture?.length || host.rewrite?.length || host.response?.length} +
+ {#if host.capture?.length} + + + {host.capture.length} capture + + {/if} + {#if host.rewrite?.length} + + + {host.rewrite.length} rewrite + + {/if} + {#if host.response?.length} + + + {host.response.length} response + + {/if}
{/if} - {#if configData.hosts[expandedHostIndex].access} -
- - Access Mode - - Private requires visiting a lure URL first (recommended) -
- {#if configData.hosts[expandedHostIndex].access?.mode === 'private'} -
- - On Deny - - Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) -
- {/if} - {/if} -
- {:else if currentHostTab === 'capture'} -
-

Extract credentials, tokens, and other data from requests and responses.

-
-
- {#each configData.hosts[expandedHostIndex].capture || [] as rule, ruleIndex (rule._id)} -
-
- {rule.name || `Rule ${ruleIndex + 1}`} - -
-
-
- - Name - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.name`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.name` - )} - {/if} -
-
- - Method - -
-
- - Path (regex) - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.path`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.path` - )} - {/if} -
-
- handleCaptureEngineChange(rule, val)} - > - Engine - -
- {#if rule.engine !== 'cookie'} -
- - From - -
- {/if} -
- - {#if rule.engine === 'regex'} - Regex Pattern - {:else if rule.engine === 'header'} - Header Name - {:else if rule.engine === 'cookie'} - Cookie Name - {:else if rule.engine === 'json'} - JSON Path - {:else} - Field Name - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.find`)} - {getError( - `hosts.${expandedHostIndex}.capture.${ruleIndex}.find` - )} - {/if} -
-
- - Must be captured before session completes and campaign flow - progresses -
-
- - Event Type - -
-
-
- {/each} - -
- {:else if currentHostTab === 'rewrite'} -
-

- Rewrite rules modify content passing through the proxy. Use Regex - for text replacement or DOM for HTML element manipulation. -

-
-
- {#each configData.hosts[expandedHostIndex].rewrite || [] as rule, ruleIndex (rule._id)} -
-
- {rule.name || `Rule ${ruleIndex + 1}`} - -
-
-
- - Name - -
-
- handleRewriteEngineChange(rule, rule.engine)} - > - Engine - -
-
- - Path (regex, optional) - - Only apply this rule when the request path matches -
-
- ({ value: m, label: m })) - ]} - size="normal" - > - Method (optional) - -
- {#if rule.engine === 'dom'} -
- - Action - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.action`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.action` - )} - {/if} -
-
- - Target - - Also supports numeric list (1,3,5) or range (2-4) -
- {:else if rule.engine === 'header'} -
- - Action - -
- {/if} -
- - From - -
-
- - {#if rule.engine === 'dom'} - Selector (CSS) - {:else if rule.engine === 'header'} - Header Name - {:else} - Find (regex) - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.find`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.find` - )} - {:else} - - {#if rule.engine === 'dom'} - CSS selector to find HTML elements - {:else if rule.engine === 'header'} - Exact header name (e.g. Server, X-Powered-By) - {:else} - Regex pattern to search for in content - {/if} - - {/if} -
- {#if rule.engine !== 'header' || rule.action !== 'remove'} -
- - {#if rule.engine === 'dom' && rule.action === 'setAttr'} - Value (attr:value) - {:else if rule.engine === 'dom' && rule.action === 'remove'} - Value (not required) - {:else if rule.engine === 'header'} - New Value - {:else} - Replace - {/if} - - {#if hasError(`hosts.${expandedHostIndex}.rewrite.${ruleIndex}.replace`)} - {getError( - `hosts.${expandedHostIndex}.rewrite.${ruleIndex}.replace` - )} - {:else} - - {#if rule.engine === 'dom'} - {#if rule.action === 'setAttr'} - Format: attribute:value (e.g. href:https://example.com) - {:else if rule.action === 'remove'} - Not required for remove action - {:else if rule.action === 'removeAttr'} - Attribute name to remove - {:else if rule.action === 'addClass' || rule.action === 'removeClass'} - CSS class name - {:else} - New content for matched elements - {/if} - {:else if rule.engine === 'header'} - New value for the header - {:else} - Replacement text (use $1, $2 for capture groups) - {/if} - - {/if} -
- {/if} -
-
- {/each} - -
- {:else if currentHostTab === 'response'} -
-

- Return custom responses for specific paths instead of proxying to the target. -

-
-
- {#each configData.hosts[expandedHostIndex].response || [] as rule, ruleIndex (rule._id)} -
-
- {rule.path || `Rule ${ruleIndex + 1}`} - -
-
-
- - Path - - {#if hasError(`hosts.${expandedHostIndex}.response.${ruleIndex}.path`)} - {getError( - `hosts.${expandedHostIndex}.response.${ruleIndex}.path` - )} - {/if} -
-
- - Status - -
-
- - Body - -
-
-
-
- Headers - -
- {#if rule.headers && Object.keys(rule.headers).length > 0} -
- {#each Object.entries(rule.headers) as [key, value]} -
- - updateResponseHeaderKey(rule, key, e.currentTarget.value)} - placeholder="Header-Name" - class="header-key-input" - /> - - -
- {/each} -
- {/if} - Use {'{{.Origin}}'} to echo the request's Origin header -
-
-
- -
-
-
- {/each} - -
- {:else if currentHostTab === 'urlrewrite'} -
-

Transform URL paths to evade detection by masking original target URLs.

-
-
- {#each configData.hosts[expandedHostIndex].rewrite_urls || [] as rule, ruleIndex (rule._id)} -
-
- {rule.find || `Rule ${ruleIndex + 1}`} - -
-
-
- - Find - -
-
- - Replace - -
-
-
-
- Query Parameter Renames - -
- {#if rule.query && rule.query.length > 0} -
- {#each rule.query as qParam, qIndex} -
- - - -
- {/each} -
- {/if} - rename query parameters when rewriting the URL (original → new - name) -
-
-
-
-
- Query Parameter Filter - -
- {#if rule.filter && rule.filter.length > 0} -
- {#each rule.filter as filterParam, fIndex} -
- - -
- {/each} -
- {/if} - allowlist of query parameters to keep — if empty, all parameters - are forwarded -
-
-
-
- {/each} - -
- {/if} + + {/each}
{:else} -
- - - -

Select a host or add a new one

+
+

No hosts configured

{/if}
- {:else if activeTab === 'global'} -
-
- -
-

- - - - Security -

-
-
- - TLS Mode - - Controls certificate verification for upstream connections -
-
- - Access Mode - - Private requires visiting a lure URL first (recommended) -
- {#if configData.global.access?.mode === 'private'} -
- - On Deny - - Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) -
- {/if} -
-
- -
-

- +
+ {#if expandedHostIndex >= 0 && configData.hosts[expandedHostIndex]} +
+
+ {configData.hosts[expandedHostIndex].to || 'New Host'} - - - Client Browser Impersonation -

-
- - Detects client browser and uses a matching fingerprint profile (Chrome or Firefox - only, others default to Chrome)→ + {configData.hosts[expandedHostIndex].domain || 'target'} - {#if configData.global.impersonate.enabled} -
+
+ + +
+
+ + +
+ +
+ + + +
- -
-

- - - - Template Variables -

-
- - Allow template variables like {'{{.Email}}'} in rewrite rules to be replaced - with recipient data - {#if configData.global.variables.enabled} -
- - Leave empty to allow all variables, or select specific ones to restrict which - can be used +
+
+ + Phishing Domain + + {#if hasError(`hosts.${expandedHostIndex}.to`)} + {getError(`hosts.${expandedHostIndex}.to`)} + {:else} + Your phishing domain that will serve the content + {/if} +
+
+ + Target Domain + + {#if hasError(`hosts.${expandedHostIndex}.domain`)} + {getError(`hosts.${expandedHostIndex}.domain`)} + {:else} + The legitimate domain being impersonated + {/if} +
+
+ + Scheme + +
+ {#if configData.hosts[expandedHostIndex].tls} +
+ + TLS Mode +
{/if} + {#if configData.hosts[expandedHostIndex].access} +
+ + Access Mode + + Private requires visiting a lure URL first (recommended) +
+ {#if configData.hosts[expandedHostIndex].access?.mode === 'private'} +
+ + On Deny + + Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) +
+ {/if} + {/if}
-
-
- - -
-
- - - - -
- -
- {#if !globalRulesTab || globalRulesTab === 'capture'} + {#if currentHostTab === 'capture'}

Extract credentials, tokens, and other data from requests and responses.

- {#each configData.global.capture || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].capture || [] as rule, ruleIndex (rule._id)}
- {rule.name || `Rule ${i + 1}`} + {rule.name || `Rule ${ruleIndex + 1}`}
Path (regex) - {#if hasError(`global.capture.${i}.path`)} - {getError(`global.capture.${i}.path`)} + {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.path`)} + {getError( + `hosts.${expandedHostIndex}.capture.${ruleIndex}.path` + )} {/if}
{#if rule.engine === 'regex'} Regex Pattern @@ -2888,8 +1916,12 @@ Field Name {/if} - {#if hasError(`global.capture.${i}.find`)} - {getError(`global.capture.${i}.find`)} + {#if hasError(`hosts.${expandedHostIndex}.capture.${ruleIndex}.find`)} + {getError( + `hosts.${expandedHostIndex}.capture.${ruleIndex}.find` + )} {/if}
@@ -2911,7 +1943,7 @@
{/each} -
- {:else if globalRulesTab === 'rewrite'} + {:else if currentHostTab === 'rewrite'}

Rewrite rules modify content passing through the proxy. Use

- {#each configData.global.rewrite || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].rewrite || [] as rule, ruleIndex (rule._id)}
- {rule.name || `Rule ${i + 1}`} + {rule.name || `Rule ${ruleIndex + 1}`}
- {:else if globalRulesTab === 'response'} + {:else if currentHostTab === 'response'}

Return custom responses for specific paths instead of proxying to the target.

- {#each configData.global.response || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].response || [] as rule, ruleIndex (rule._id)}
- {rule.path || `Rule ${i + 1}`} + {rule.path || `Rule ${ruleIndex + 1}`}
@@ -3265,11 +2319,11 @@ { rule.forward = e.currentTarget.checked; configData = configData; }} - class="checkbox-input" /> Forward to target @@ -3277,26 +2331,30 @@
{/each} -
- {:else if globalRulesTab === 'urlrewrite'} + {:else if currentHostTab === 'urlrewrite'}

Transform URL paths to evade detection by masking original target URLs.

- {#each configData.global.rewrite_urls || [] as rule, i (rule._id)} + {#each configData.hosts[expandedHostIndex].rewrite_urls || [] as rule, ruleIndex (rule._id)}
- {rule.find || `Rule ${i + 1}`} + {rule.find || `Rule ${ruleIndex + 1}`}
{/each} -
{/if}
+ {:else} +
+ + + +

Select a host or add a new one

+ +
+ {/if} +
+
+
+
+ +
+

+ + + + Security +

+
+
+ + TLS Mode + + Controls certificate verification for upstream connections +
+ {#if configData.global.tls.mode === 'custom'} +
+ onGlobalCertFile(e, 'globalTLSKey')} + >Private Key (.key) + leave empty to keep existing certificate +
+
+ onGlobalCertFile(e, 'globalTLSPem')} + >Certificate (.pem, .crt) + leave empty to keep existing certificate +
+ {/if} +
+ + Access Mode + + Private requires visiting a lure URL first (recommended) +
+ {#if configData.global.access?.mode === 'private'} +
+ + On Deny + + Status code (e.g. 404, 503) or redirect URL (e.g. https://example.com) +
+ {/if} +
+
+ + +
+

+ + + + Client Browser Impersonation +

+
+ + Detects client browser and uses a matching fingerprint profile (Chrome or Firefox + only, others default to Chrome) + {#if configData.global.impersonate.enabled} + + Use the client's User-Agent header instead of the impersonated browser's default + {/if} +
+
+ + +
+

+ + + + Template Variables +

+
+ + Allow template variables like {'{{.Email}}'} in rewrite rules to be replaced + with recipient data + {#if configData.global.variables.enabled} +
+ + Leave empty to allow all variables, or select specific ones to restrict which + can be used +
+ {/if} +
- {/if} + + +
+
+ + + + +
+ +
+ {#if !globalRulesTab || globalRulesTab === 'capture'} +
+

Extract credentials, tokens, and other data from requests and responses.

+
+
+ {#each configData.global.capture || [] as rule, i (rule._id)} +
+
+ {rule.name || `Rule ${i + 1}`} + +
+
+
+ + Name + + {#if hasError(`global.capture.${i}.name`)} + {getError(`global.capture.${i}.name`)} + {/if} +
+
+ + Method + +
+
+ + Path (regex) + + {#if hasError(`global.capture.${i}.path`)} + {getError(`global.capture.${i}.path`)} + {/if} +
+
+ handleCaptureEngineChange(rule, val)} + > + Engine + +
+ {#if rule.engine !== 'cookie'} +
+ + From + +
+ {/if} +
+ + {#if rule.engine === 'regex'} + Regex Pattern + {:else if rule.engine === 'header'} + Header Name + {:else if rule.engine === 'cookie'} + Cookie Name + {:else if rule.engine === 'json'} + JSON Path + {:else} + Field Name + {/if} + + {#if hasError(`global.capture.${i}.find`)} + {getError(`global.capture.${i}.find`)} + {/if} +
+
+ + Must be captured before session completes and campaign flow progresses +
+
+ + Event Type + +
+
+
+ {/each} + +
+ {:else if globalRulesTab === 'rewrite'} +
+

+ Rewrite rules modify content passing through the proxy. Use Regex + for text replacement or DOM for HTML element manipulation. +

+
+
+ {#each configData.global.rewrite || [] as rule, i (rule._id)} +
+
+ {rule.name || `Rule ${i + 1}`} + +
+
+
+ + Name + +
+
+ handleRewriteEngineChange(rule, rule.engine)} + > + Engine + +
+
+ + Path (regex, optional) + + Only apply this rule when the request path matches +
+
+ ({ value: m, label: m })) + ]} + size="normal" + > + Method (optional) + +
+ {#if rule.engine === 'dom'} +
+ + Action + + {#if hasError(`global.rewrite.${i}.action`)} + {getError(`global.rewrite.${i}.action`)} + {/if} +
+
+ + Target + + Also supports numeric list (1,3,5) or range (2-4) +
+ {:else if rule.engine === 'header'} +
+ + Action + +
+ {/if} +
+ + From + +
+
+ + {#if rule.engine === 'dom'} + Selector (CSS) + {:else if rule.engine === 'header'} + Header Name + {:else} + Find (regex) + {/if} + + {#if hasError(`global.rewrite.${i}.find`)} + {getError(`global.rewrite.${i}.find`)} + {:else} + + {#if rule.engine === 'dom'} + CSS selector to find HTML elements + {:else if rule.engine === 'header'} + Exact header name (e.g. Server, X-Powered-By) + {:else} + Regex pattern to search for in content + {/if} + + {/if} +
+ {#if rule.engine !== 'header' || rule.action !== 'remove'} +
+ + {#if rule.engine === 'dom' && rule.action === 'setAttr'} + Value (attr:value) + {:else if rule.engine === 'dom' && rule.action === 'remove'} + Value (not required) + {:else if rule.engine === 'header'} + New Value + {:else} + Replace + {/if} + + {#if hasError(`global.rewrite.${i}.replace`)} + {getError(`global.rewrite.${i}.replace`)} + {:else} + + {#if rule.engine === 'dom'} + {#if rule.action === 'setAttr'} + Format: attribute:value (e.g. href:https://example.com) + {:else if rule.action === 'remove'} + Not required for remove action + {:else if rule.action === 'removeAttr'} + Attribute name to remove + {:else if rule.action === 'addClass' || rule.action === 'removeClass'} + CSS class name + {:else} + New content for matched elements + {/if} + {:else if rule.engine === 'header'} + New value for the header + {:else} + Replacement text (use $1, $2 for capture groups) + {/if} + + {/if} +
+ {/if} +
+
+ {/each} + +
+ {:else if globalRulesTab === 'response'} +
+

Return custom responses for specific paths instead of proxying to the target.

+
+
+ {#each configData.global.response || [] as rule, i (rule._id)} +
+
+ {rule.path || `Rule ${i + 1}`} + +
+
+
+ + Path + + {#if hasError(`global.response.${i}.path`)} + {getError(`global.response.${i}.path`)} + {/if} +
+
+ + Status + +
+
+ + Body + +
+
+
+
+ Headers + +
+ {#if rule.headers && Object.keys(rule.headers).length > 0} +
+ {#each Object.entries(rule.headers) as [key, value]} +
+ + updateResponseHeaderKey(rule, key, e.currentTarget.value)} + placeholder="Header-Name" + class="header-key-input" + /> + + +
+ {/each} +
+ {/if} + Use {'{{.Origin}}'} to echo the request's Origin header +
+
+
+ +
+
+
+ {/each} + +
+ {:else if globalRulesTab === 'urlrewrite'} +
+

Transform URL paths to evade detection by masking original target URLs.

+
+
+ {#each configData.global.rewrite_urls || [] as rule, i (rule._id)} +
+
+ {rule.find || `Rule ${i + 1}`} + +
+
+
+ + Find + +
+
+ + Replace + +
+
+
+
+ Query Parameter Renames + +
+ {#if rule.query && rule.query.length > 0} +
+ {#each rule.query as qParam, qIndex} +
+ + + +
+ {/each} +
+ {/if} + rename query parameters when rewriting the URL (original → new name) +
+
+
+
+
+ Query Parameter Filter + +
+ {#if rule.filter && rule.filter.length > 0} +
+ {#each rule.filter as filterParam, fIndex} +
+ + +
+ {/each} +
+ {/if} + allowlist of query parameters to keep — if empty, all parameters are + forwarded +
+
+
+
+ {/each} + +
+ {/if} +
+
+
diff --git a/frontend/src/routes/domain/+page.svelte b/frontend/src/routes/domain/+page.svelte index 916b9ff..16cdab6 100644 --- a/frontend/src/routes/domain/+page.svelte +++ b/frontend/src/routes/domain/+page.svelte @@ -853,9 +853,9 @@ onSetFile(e, 'ownManagedTLSPem')} - >Certificate (.pem)Certificate (.pem, .crt)
{/if} diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index 697d9b8..0107bb2 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -30,6 +30,7 @@ import AutoRefresh from '$lib/components/AutoRefresh.svelte'; import TableCellScope from '$lib/components/table/TableCellScope.svelte'; import ProxyConfigBuilder from '$lib/components/proxy/ProxyConfigBuilder.svelte'; + import FileField from '$lib/components/FileField.svelte'; // services const appStateService = AppStateService.instance; @@ -46,6 +47,24 @@ }; let isSubmitting = false; + // cert state for custom TLS mode — populated by certChange event (visual mode) or yaml cert upload fields + let globalTLSKey = ''; + let globalTLSPem = ''; + + // detect if the current yaml config uses tls.mode: "custom" anywhere so we can show upload fields in yaml mode + $: yamlHasCustomTLS = (() => { + if (!formValues.proxyConfig || editorMode !== 'yaml') return false; + try { + // simple string check first to avoid parsing on every keystroke + if (!formValues.proxyConfig.includes('custom')) return false; + // dynamic import not available in reactive block — use a regex check on the yaml string + // matches: mode: "custom" or mode: 'custom' or mode: custom + return /mode\s*:\s*['"]?custom['"]?/.test(formValues.proxyConfig); + } catch { + return false; + } + })(); + // data const tableURLParams = newTableURLParams(); let contextCompanyID = null; @@ -212,7 +231,7 @@ # global configuration (applies to all hosts unless overridden) global: tls: - mode: "managed" # "managed" (Let's Encrypt) or "self-signed" + mode: "managed" # "managed" (Let's Encrypt), "self-signed", or "custom" (upload cert below) # template variables allow recipient data in rewrite rules # variables: # enabled: true @@ -225,7 +244,7 @@ portal.example.com: # scheme: "https" # use https:// when connecting to target # optional: override global TLS config for this specific host # tls: - # mode: "self-signed" + # mode: "self-signed" # or "custom" for a manually supplied certificate response: - path: "^/api/health$" headers: @@ -393,8 +412,13 @@ portal.example.com: }; const res = await api.proxy.create({ - ...proxyData, - companyID: contextCompanyID + name: proxyData.name, + description: proxyData.description, + startURL: proxyData.startURL, + proxyConfig: proxyData.proxyConfig, + companyID: contextCompanyID, + globalTLSKey: globalTLSKey || undefined, + globalTLSPem: globalTLSPem || undefined }); if (!res.success) { formError = res.error; @@ -416,7 +440,9 @@ portal.example.com: name: formValues.name, description: formValues.description, startURL: formValues.startURL, - proxyConfig: formValues.proxyConfig + proxyConfig: formValues.proxyConfig, + globalTLSKey: globalTLSKey || undefined, + globalTLSPem: globalTLSPem || undefined }; const res = await api.proxy.update(formValues.id, updateData); @@ -469,6 +495,26 @@ portal.example.com: formValues.id = ''; form.reset(); formError = ''; + globalTLSKey = ''; + globalTLSPem = ''; + }; + + /** + * @param {Event} event + * @param {'key'|'pem'} target + */ + const onYamlCertFile = (event, target) => { + const file = /** @type {HTMLInputElement} */ (event.target).files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + if (target === 'key') { + globalTLSKey = e.target?.result?.toString() ?? ''; + } else { + globalTLSPem = e.target?.result?.toString() ?? ''; + } + }; + reader.readAsText(file); }; /** @param {string} id */ @@ -719,6 +765,22 @@ portal.example.com: >
+ {#if yamlHasCustomTLS} +
+ onYamlCertFile(e, 'key')}>Private Key (.key) + onYamlCertFile(e, 'pem')}>Certificate (.pem, .crt) +
+ {/if}
{/if} @@ -830,6 +892,10 @@ portal.example.com: on:nameChange={(e) => (formValues.name = e.detail)} on:descriptionChange={(e) => (formValues.description = e.detail)} on:startURLChange={(e) => (formValues.startURL = e.detail)} + on:certChange={(e) => { + globalTLSKey = e.detail.globalTLSKey || ''; + globalTLSPem = e.detail.globalTLSPem || ''; + }} /> {/key}