Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 107 additions & 3 deletions common/httpclient.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package common

import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"sync/atomic"

"github.com/getlantern/amp"
"github.com/getlantern/dnstt"
"github.com/getlantern/fronted"
"github.com/getlantern/kindling"
Expand Down Expand Up @@ -39,8 +45,9 @@ var (
"service.dogsdogs.xyz", // Used in replica
}

configDir atomic.Value
dnsttConfig atomic.Value // Holds the DNSTTConfig
configDir atomic.Value
dnsttConfig atomic.Value // Holds the DNSTTConfig
ampCacheConfig atomic.Value

defaultDNSTTConfig = &DNSTTConfig{
Domain: "t.iantem.io",
Expand All @@ -59,11 +66,20 @@ type DNSTTConfig struct {
UTLSDistribution string `yaml:"utlsDistribution,omitempty"`
}

// AMPCacheConfig contain the properties required for starting the
// amp cache fronting
type AMPCacheConfig struct {
BrokerURL string `yaml:"broker_url"`
CacheURL string `yaml:"cache_url"`
PublicKeyPEM string `yaml:"public_key"`
FrontDomains []string `yaml:"front_domains"`
}

func init() {
dnsttConfig.Store(defaultDNSTTConfig)
}

// The config directory on some platforms, such as Android, can only be determined in native code, so we
// SetConfigDir set the config directory on some platforms, such as Android, can only be determined in native code, so we
// need to set it externally.
func SetConfigDir(dir string) {
configDir.Store(dir)
Expand All @@ -75,13 +91,28 @@ func SetDNSTTConfig(cfg *DNSTTConfig) {
}
}

func SetAMPCacheConfig(cfg *AMPCacheConfig) {
if cfg != nil {
ampCacheConfig.Store(cfg)
}
}

func GetHTTPClient() *http.Client {
mutex.Lock()
defer mutex.Unlock()
if httpClient != nil {
return httpClient
}

return NewHTTPClient()
}

// NewHTTPClient build a new http client and store it on the httpClient
// package var. This function should be called when there's a configuration
// update.
func NewHTTPClient() *http.Client {
mutex.Lock()
defer mutex.Unlock()
var k kindling.Kindling
ioWriter := log.AsDebugLogger().Writer()
kOptions := []kindling.Option{
Expand All @@ -102,6 +133,12 @@ func GetHTTPClient() *http.Client {
} else {
kOptions = append(kOptions, kindling.WithDNSTunnel(d))
}

if ampClient, err := newAMPCache(); err != nil {
log.Errorf("Failed to create AMP cache client: %v", err)
} else {
kOptions = append(kOptions, kindling.WithAMPCache(ampClient))
}
k = kindling.NewKindling("flashlight", kOptions...)
httpClient = k.NewHTTPClient()
return httpClient
Expand Down Expand Up @@ -154,6 +191,20 @@ func (c *DNSTTConfig) Validate() error {
return nil
}

func (c *AMPCacheConfig) Validate() error {
if c.BrokerURL == "" ||
c.CacheURL == "" ||
len(c.FrontDomains) == 0 ||
c.PublicKeyPEM == "" {
return fmt.Errorf(
`missing one or more mandatory arguments for running amp cache fronting.
Please review the provided parameter values, broker URL: %q, cache URL: %q, front domains: %q, public key: %q`,
c.BrokerURL, c.CacheURL, c.FrontDomains, c.PublicKeyPEM,
)
}
return nil
}

func newDNSTT() (dnstt.DNSTT, error) {
cfg := dnsttConfig.Load().(*DNSTTConfig)
if err := cfg.Validate(); err != nil {
Expand All @@ -175,3 +226,56 @@ func newDNSTT() (dnstt.DNSTT, error) {
}
return dnstt.NewDNSTT(options...)
}

func newAMPCache() (amp.Client, error) {
cfg := ampCacheConfig.Load()
if cfg == nil {
return nil, fmt.Errorf("amp cache config not available")
}

config, ok := cfg.(*AMPCacheConfig)
if !ok {
return nil, fmt.Errorf("invalid data stored in amp cache config")
}
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid AMP cache configuration: %w", err)
}

brokerURL, err := url.Parse(config.BrokerURL)
if err != nil {
return nil, fmt.Errorf("invalid broker url: %w", err)
}

cacheURL, err := url.Parse(config.CacheURL)
if err != nil {
return nil, fmt.Errorf("invalid cache url: %w", err)
}

// parse rsa public key pem
pubKey, err := parsePublicKeyPEM(config.PublicKeyPEM)
if err != nil {
return nil, fmt.Errorf("invalid public key pem: %w", err)
}

return amp.NewClient(brokerURL, cacheURL, config.FrontDomains, nil, pubKey, func(network, address string) (net.Conn, error) {
return tls.Dial("tcp", fmt.Sprintf("%s:443", address), &tls.Config{
ServerName: address,
})
})
}

func parsePublicKeyPEM(pemStr string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(pemStr))
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block containing the public key")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse DER encoded public key: %v", err)
}
key, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not RSA public key")
}
return key, nil
}
3 changes: 3 additions & 0 deletions config/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ type Global struct {
// DNSTTConfig is the configuration for the DNS tunnel.
DNSTTConfig *common.DNSTTConfig

// AMPCacheConfig contain the configuration for amp fronting
AMPCacheConfig *common.AMPCacheConfig

// GlobalConfigPollInterval sets interval at which to poll for global config
GlobalConfigPollInterval time.Duration

Expand Down
3 changes: 3 additions & 0 deletions flashlight.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@ func (f *Flashlight) onGlobalConfig(cfg *config.Global, src config.Source) {
f.callbacks.onInit()

common.SetDNSTTConfig(cfg.DNSTTConfig)
common.SetAMPCacheConfig(cfg.AMPCacheConfig)
// building fresh http client with new configuration
common.NewHTTPClient()
}

// EnabledFeatures gets all features enabled based on current conditions
Expand Down
32 changes: 30 additions & 2 deletions genconfig/genconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var (
minFreq = flag.Float64("minfreq", 3.0, "Minimum frequency (percentage) for including CA cert in list of trusted certs, defaults to 3.0%")
numberOfWorkers = flag.Int("numworkers", 50, "Number of worker threads")
dnsttFile = flag.String("dnstt-file", "", "Path to yaml file containing DNSTT config")
ampCacheFile = flag.String("amp-cache-file", "", "Path to yaml file containing AMP cache config")

enabledProviders stringsFlag // --enable-provider in init()
masqueradesInFiles stringsFlag // --masquerades in init()
Expand Down Expand Up @@ -209,6 +210,7 @@ func (c *ConfigGenerator) GenerateConfig(
minFreq float64,
minMasquerades, maxMasquerades int,
dnsttConfig *common.DNSTTConfig,
ampCacheConfig *common.AMPCacheConfig,
) ([]byte, error) {
if err := c.loadFtVersion(); err != nil {
return nil, err
Expand All @@ -220,7 +222,7 @@ func (c *ConfigGenerator) GenerateConfig(
return nil, err
}

model, err := c.buildModel("cloud.yaml", cas, dnsttConfig)
model, err := c.buildModel("cloud.yaml", cas, dnsttConfig, ampCacheConfig)
if err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}
Expand Down Expand Up @@ -259,6 +261,11 @@ func main() {
log.Errorf("Error loading DNSTT config: %s", err)
}

ampCacheConfig, err := loadAMPCacheConfig()
if err != nil {
log.Errorf("failed to load amp cache config: %w", err)
}

yamlTmpl := string(embeddedconfig.GlobalTemplate)
template, err := generator.GenerateConfig(
context.Background(),
Expand All @@ -271,6 +278,7 @@ func main() {
*minMasquerades,
*maxMasquerades,
dnsttCfg,
ampCacheConfig,
)
if err != nil {
log.Fatalf("Error generating configuration: %s", err)
Expand Down Expand Up @@ -387,6 +395,25 @@ func loadDNSTTConfig() (*common.DNSTTConfig, error) {
return &cfg, nil
}

func loadAMPCacheConfig() (*common.AMPCacheConfig, error) {
if *ampCacheFile == "" {
return nil, nil
}

bytes, err := os.ReadFile(*ampCacheFile)
if err != nil {
return nil, fmt.Errorf("unable to read amp cache config file at %s: %s", *ampCacheFile, err)
}
var cfg common.AMPCacheConfig
if err := yaml.Unmarshal(bytes, &cfg); err != nil {
return nil, fmt.Errorf("unable to parse amp cache config file at %s: %s", *ampCacheFile, err)
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid amp cache config: %w", err)
}
return &cfg, nil
}

func loadTemplate(name string) string {
bytes, err := os.ReadFile(name)
if err != nil {
Expand Down Expand Up @@ -602,7 +629,7 @@ func (c *ConfigGenerator) doVetMasquerades(certPool *x509.CertPool, inCh chan *m
c.wg.Done()
}

func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat, dnsttCfg *common.DNSTTConfig) (map[string]interface{}, error) {
func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat, dnsttCfg *common.DNSTTConfig, ampCacheCfg *common.AMPCacheConfig) (map[string]interface{}, error) {
casList := make([]*castat, 0, len(cas))
for _, ca := range cas {
casList = append(casList, ca)
Expand Down Expand Up @@ -652,6 +679,7 @@ func (c *ConfigGenerator) buildModel(configName string, cas map[string]*castat,
"proxiedsites": ps,
"ftVersion": c.ftVersion,
"dnstt": dnsttCfg,
"amp_cache": ampCacheCfg,
}, nil
}

Expand Down
61 changes: 48 additions & 13 deletions genconfig/genconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ func TestGenerateConfig(t *testing.T) {
UTLSDistribution: "chrome",
}

ampCacheConfig := &common.AMPCacheConfig{
BrokerURL: "https://amp.broker.com",
CacheURL: "https://amp.cache.com",
PublicKeyPEM: "pem",
FrontDomains: []string{"pudim.com.br"},
}

defaultSetup := func(ctrl *gomock.Controller) *ConfigGenerator {
configGenerator := NewConfigGenerator()

verifier := NewMockverifier(ctrl)
verifier.EXPECT().Vet(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
configGenerator.verifier = verifier

certGrabber := NewMockcertGrabber(ctrl)
certGrabber.EXPECT().GetCertificate(gomock.Any(), gomock.Any()).Return(cert, nil).AnyTimes()
configGenerator.certGrabber = certGrabber

return configGenerator
}

var tests = []struct {
name string
givenContext context.Context
Expand All @@ -67,6 +88,7 @@ func TestGenerateConfig(t *testing.T) {
givenMinMasquerades int
givenMaxMasquerades int
givenDNSTTConfig *common.DNSTTConfig
givenAMPCacheConfig *common.AMPCacheConfig
assert func(*testing.T, string, error)
setup func(*gomock.Controller) *ConfigGenerator
}{
Expand All @@ -82,6 +104,7 @@ func TestGenerateConfig(t *testing.T) {
givenMinMasquerades: 1,
givenMaxMasquerades: 10,
givenDNSTTConfig: dnsttCfg,
givenAMPCacheConfig: ampCacheConfig,
assert: func(t *testing.T, cfg string, err error) {
require.NoError(t, err)
require.NotEmpty(t, cfg)
Expand Down Expand Up @@ -161,20 +184,31 @@ func TestGenerateConfig(t *testing.T) {
},
},
{
name: "nil DNSTT config",
setup: func(ctrl *gomock.Controller) *ConfigGenerator {
configGenerator := NewConfigGenerator()

verifier := NewMockverifier(ctrl)
verifier.EXPECT().Vet(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
configGenerator.verifier = verifier

certGrabber := NewMockcertGrabber(ctrl)
certGrabber.EXPECT().GetCertificate(gomock.Any(), gomock.Any()).Return(cert, nil).AnyTimes()
configGenerator.certGrabber = certGrabber
name: "nil DNSTT config",
setup: defaultSetup,
givenContext: ctx,
givenTemplate: globalTemplateTest,
givenMasquerades: masquerades,
givenProxiedSites: proxiedSites,
givenBlacklist: blacklist,
givenNumberOfWorkers: 10,
givenMinFrequency: 10,
givenMinMasquerades: 1,
givenMaxMasquerades: 10,
assert: func(t *testing.T, cfg string, err error) {
require.NoError(t, err)
require.NotEmpty(t, cfg)
require.NoError(t, err)

return configGenerator
globalConfig, err := parseGlobal(ctx, []byte(cfg))
require.NoError(t, err)
assert.NotNil(t, globalConfig)
assert.Nil(t, globalConfig.DNSTTConfig, "DNSTT config should be nil when not provided")
},
},
{
name: "nil amp config",
setup: defaultSetup,
givenContext: ctx,
givenTemplate: globalTemplateTest,
givenMasquerades: masquerades,
Expand All @@ -192,7 +226,7 @@ func TestGenerateConfig(t *testing.T) {
globalConfig, err := parseGlobal(ctx, []byte(cfg))
require.NoError(t, err)
assert.NotNil(t, globalConfig)
assert.Nil(t, globalConfig.DNSTTConfig, "DNSTT config should be nil when not provided")
assert.Nil(t, globalConfig.AMPCacheConfig, "AMP cache config should be nil when not provided")
},
},
}
Expand All @@ -214,6 +248,7 @@ func TestGenerateConfig(t *testing.T) {
tt.givenMinMasquerades,
tt.givenMaxMasquerades,
tt.givenDNSTTConfig,
tt.givenAMPCacheConfig,
)
tt.assert(t, string(cfg), err)
})
Expand Down
Loading
Loading