Skip to content

Commit

Permalink
Merge pull request #171 from launchdarkly/eb/ch87406/undelete-core
Browse files Browse the repository at this point in the history
(v6 - #1) revert deletion of core packages, don't use ld-relay-core
  • Loading branch information
eli-darkly authored Aug 28, 2020
2 parents ca35feb + fe45b11 commit b7eb84d
Show file tree
Hide file tree
Showing 134 changed files with 11,588 additions and 287 deletions.
8 changes: 1 addition & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
# Contributing to the LaunchDarkly Relay Proxy

The LaunchDarkly Relay Proxy's functionality is closely related to the LaunchDarkly SDKs'. We suggest that you review the LaunchDarkly [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) if you are working on this code.

The Relay Proxy code is distributed across several repositories:

* [`github.com/launchdarkly/ld-relay-core`](https://github.com/launchdarkly/ld-relay-core): This contains components that are also shared with Relay Proxy Enterprise. Most of the Relay Proxy functionality is implemented here.
* [`github.com/launchdarkly/ld-relay-config`](https://github.com/launchdarkly/ld-relay-config): This contains configuration types which are also shared with Relay Proxy Enterprise. This is separate from `ld-relay-core` because it is a public, supported API.
* This repository, `ld-relay`, which contains only the high-level interface to the Relay Proxy when it is either built as an application or imported as a library.


## Submitting bug reports and feature requests

The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/ld-relay/issues) in this repository. File bug reports and feature requests specific to the Relay Proxy in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
Expand Down
88 changes: 88 additions & 0 deletions core/application/commandline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package application

import (
"flag"
"fmt"
"io"
"os"
"strings"
)

// DefaultConfigPath is the default configuration file path.
const DefaultConfigPath = "/etc/ld-relay.conf"

// Options represents all options that can be set from the command line.
type Options struct {
ConfigFile string
AllowMissingFile bool
UseEnvironment bool
}

func errConfigFileNotFound(filename string) error {
return fmt.Errorf("configuration file %q does not exist", filename)
}

// DescribeConfigSource returns a human-readable phrase describing whether the configuration comes from a
// file, from variables, or both.
func (o Options) DescribeConfigSource() string {
if o.ConfigFile == "" && o.UseEnvironment {
return "configuration from environment variables"
}
desc := ""
if o.ConfigFile != "" {
desc = fmt.Sprintf("configuration file %s", o.ConfigFile)
}
if o.UseEnvironment {
desc += " plus environment variables"
}
return desc
}

// ReadOptions reads and validates the command-line options.
//
// The configuration parameter behavior is as follows:
// 1. If you specify --config $FILEPATH, it loads that file. Failure to find it or parse it is a fatal error,
// unless you also specify --allow-missing-file.
// 2. If you specify --from-env, it creates a configuration from environment variables as described in README.
// 3. If you specify both, the file is loaded first, then it applies changes from variables if any.
// 4. Omitting all options is equivalent to explicitly specifying --config /etc/ld-relay.conf.
func ReadOptions(osArgs []string, errorOutput io.Writer) (Options, error) {
var o Options

fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.SetOutput(errorOutput)
fs.StringVar(&o.ConfigFile, "config", "", "configuration file location")
fs.BoolVar(&o.AllowMissingFile, "allow-missing-file", false, "suppress error if config file is not found")
fs.BoolVar(&o.UseEnvironment, "from-env", false, "read configuration from environment variables")
err := fs.Parse(osArgs[1:])
if err != nil {
return o, err
}

if o.ConfigFile == "" && !o.UseEnvironment {
o.ConfigFile = DefaultConfigPath
}

if o.ConfigFile != "" {
_, err := os.Stat(o.ConfigFile)
fileExists := err == nil || !os.IsNotExist(err)
if !fileExists {
if !o.AllowMissingFile {
return o, errConfigFileNotFound(o.ConfigFile)
}
o.ConfigFile = ""
}
}

return o, nil
}

// DescribeRelayVersion returns the same version string unless it is a prerelease build, in
// which case it is reformatted to change "+xxx" into "(build xxx)".
func DescribeRelayVersion(version string) string {
split := strings.Split(version, "+")
if len(split) == 2 {
return fmt.Sprintf("%s (build %s)", split[0], split[1])
}
return version
}
61 changes: 61 additions & 0 deletions core/application/commandline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package application

import (
"io/ioutil"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

helpers "github.com/launchdarkly/go-test-helpers/v2"
)

func TestReadOptions(t *testing.T) {
appName := "ld-relay"

t.Run("default config file path", func(t *testing.T) {
_, err := ReadOptions([]string{appName}, ioutil.Discard)
require.Error(t, err)
assert.Equal(t, errConfigFileNotFound(DefaultConfigPath), err)
})

t.Run("allow missing file with default path", func(t *testing.T) {
opts, err := ReadOptions([]string{appName, "--allow-missing-file"}, ioutil.Discard)
require.NoError(t, err)
assert.Equal(t, "", opts.ConfigFile)
assert.False(t, opts.UseEnvironment)
})

t.Run("custom config file", func(t *testing.T) {
helpers.WithTempFile(func(filename string) {
opts, err := ReadOptions([]string{appName, "--config", filename}, ioutil.Discard)
require.NoError(t, err)
assert.Equal(t, filename, opts.ConfigFile)
assert.False(t, opts.UseEnvironment)
assert.Equal(t, "configuration file "+filename, opts.DescribeConfigSource())
})
})

t.Run("environment only", func(t *testing.T) {
opts, err := ReadOptions([]string{appName, "--from-env"}, ioutil.Discard)
require.NoError(t, err)
assert.Equal(t, "", opts.ConfigFile)
assert.True(t, opts.UseEnvironment)
assert.Equal(t, "configuration from environment variables", opts.DescribeConfigSource())
})

t.Run("environment plus config file", func(t *testing.T) {
helpers.WithTempFile(func(filename string) {
opts, err := ReadOptions([]string{appName, "--config", filename, "--from-env"}, ioutil.Discard)
require.NoError(t, err)
assert.Equal(t, filename, opts.ConfigFile)
assert.True(t, opts.UseEnvironment)
assert.Equal(t, "configuration file "+filename+" plus environment variables", opts.DescribeConfigSource())
})
})

t.Run("invalid options", func(t *testing.T) {
_, err := ReadOptions([]string{appName, "--unknown"}, ioutil.Discard)
assert.Error(t, err)
})
}
2 changes: 2 additions & 0 deletions core/application/package_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package application providers helpers used by the command-line entry points of all versions of Relay.
package application
54 changes: 54 additions & 0 deletions core/application/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package application

import (
"crypto/tls"
"fmt"
"net/http"

config "github.com/launchdarkly/ld-relay-config"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
)

// StartHTTPServer starts the server, with or without TLS. It returns immediately, starting the server
// on a separate goroutine; if the server fails to start up, it sends an error to the error channel.
func StartHTTPServer(
port int,
handler http.Handler,
tlsEnabled bool,
tlsCertFile, tlsKeyFile string,
tlsMinVersion uint16,
loggers ldlog.Loggers,
) <-chan error {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: handler,
}

if tlsEnabled && tlsMinVersion != 0 {
srv.TLSConfig = &tls.Config{
MinVersion: tlsMinVersion,
}
}

errCh := make(chan error)

go func() {
var err error
loggers.Infof("Starting server listening on port %d\n", port)
if tlsEnabled {
message := "TLS enabled for server"
if tlsMinVersion != 0 {
message += fmt.Sprintf(" (minimum TLS version: %s)", config.NewOptTLSVersion(tlsMinVersion).String())
}
loggers.Info(message)
err = srv.ListenAndServeTLS(tlsCertFile, tlsKeyFile)
} else {
err = srv.ListenAndServe()
}
if err != nil {
errCh <- err
}
}()

return errCh
}
51 changes: 51 additions & 0 deletions core/client-side.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package core

import (
"bytes"
"encoding/base64"
"net/http"
"net/http/httptest"
"strconv"

"github.com/launchdarkly/ld-relay/v6/core/internal/browser"
"github.com/launchdarkly/ld-relay/v6/core/internal/events"
"github.com/launchdarkly/ld-relay/v6/core/internal/util"
"github.com/launchdarkly/ld-relay/v6/core/middleware"
)

func getEventsImage(w http.ResponseWriter, req *http.Request) {
clientCtx := middleware.GetEnvContextInfo(req.Context())

if clientCtx.Env.GetEventDispatcher() == nil {
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write(util.ErrorJSONMsg("Event proxy is not enabled for this environment"))
return
}
handler := clientCtx.Env.GetEventDispatcher().GetHandler(events.JavaScriptSDKEventsEndpoint)
if handler == nil {
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write(util.ErrorJSONMsg("Event proxy for browser clients is not enabled for this environment"))
return
}

d := req.URL.Query().Get("d")
if d != "" {
go func() {
nullW := httptest.NewRecorder()
eventData, _ := base64.StdEncoding.DecodeString(d)
eventsReq, _ := http.NewRequest("POST", "", bytes.NewBuffer(eventData))
eventsReq.Header.Add("Content-Type", "application/json")
eventsReq.Header.Add("X-LaunchDarkly-User-Agent", eventsReq.Header.Get("X-LaunchDarkly-User-Agent"))
eventsReq.Header.Add(events.EventSchemaHeader, strconv.Itoa(events.SummaryEventsSchemaVersion))
handler(nullW, eventsReq)
}()
}

w.Header().Set("Content-Type", "image/gif")
_, _ = w.Write(browser.Transparent1PixelImageData)
}

func getGoals(w http.ResponseWriter, req *http.Request) {
clientCtx := middleware.GetEnvContextInfo(req.Context())
clientCtx.Env.GetJSClientContext().Proxy.ServeHTTP(w, req)
}
88 changes: 88 additions & 0 deletions core/httpconfig/httpconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Package httpconfig provides helpers for special types of HTTP client configuration supported by Relay.
package httpconfig

import (
"errors"
"net/http"

config "github.com/launchdarkly/ld-relay-config"
"gopkg.in/launchdarkly/go-sdk-common.v2/ldlog"
"gopkg.in/launchdarkly/go-server-sdk.v5/interfaces"
"gopkg.in/launchdarkly/go-server-sdk.v5/ldcomponents"
"gopkg.in/launchdarkly/go-server-sdk.v5/ldhttp"
"gopkg.in/launchdarkly/go-server-sdk.v5/ldntlm"
)

var (
errNTLMProxyAuthWithoutCredentials = errors.New("NTLM proxy authentication requires username and password")
errProxyAuthWithoutProxyURL = errors.New("cannot specify proxy authentication without a proxy URL")
)

// HTTPConfig encapsulates ProxyConfig plus any other HTTP options we may support in the future (currently none).
type HTTPConfig struct {
config.ProxyConfig
SDKHTTPConfigFactory interfaces.HTTPConfigurationFactory
SDKHTTPConfig interfaces.HTTPConfiguration
}

// NewHTTPConfig validates all of the HTTP-related options and returns an HTTPConfig if successful.
func NewHTTPConfig(proxyConfig config.ProxyConfig, authKey config.SDKCredential, userAgent string, loggers ldlog.Loggers) (HTTPConfig, error) {
configBuilder := ldcomponents.HTTPConfiguration()
configBuilder.UserAgent(userAgent)

ret := HTTPConfig{ProxyConfig: proxyConfig}

authKeyStr := ""
if authKey != nil {
authKeyStr = authKey.GetAuthorizationHeaderValue()
}

if !proxyConfig.URL.IsDefined() && proxyConfig.NTLMAuth {
return ret, errProxyAuthWithoutProxyURL
}
if proxyConfig.URL.IsDefined() {
loggers.Infof("Using proxy server at %s", proxyConfig.URL)
}

caCertFiles := proxyConfig.CACertFiles.Values()

if proxyConfig.NTLMAuth {
if proxyConfig.User == "" || proxyConfig.Password == "" {
return ret, errNTLMProxyAuthWithoutCredentials
}
transportOpts := []ldhttp.TransportOption{
ldhttp.ConnectTimeoutOption(ldcomponents.DefaultConnectTimeout),
}
for _, filePath := range caCertFiles {
if filePath != "" {
transportOpts = append(transportOpts, ldhttp.CACertFileOption(filePath))
}
}
factory, err := ldntlm.NewNTLMProxyHTTPClientFactory(proxyConfig.URL.String(),
proxyConfig.User, proxyConfig.Password, proxyConfig.Domain, transportOpts...)
if err != nil {
return ret, err
}
configBuilder.HTTPClientFactory(factory)
loggers.Info("NTLM proxy authentication enabled")
} else {
if proxyConfig.URL.IsDefined() {
configBuilder.ProxyURL(proxyConfig.URL.String())
}
for _, filePath := range caCertFiles {
if filePath != "" {
configBuilder.CACertFile(filePath)
}
}
}

var err error
ret.SDKHTTPConfigFactory = configBuilder
ret.SDKHTTPConfig, err = configBuilder.CreateHTTPConfiguration(interfaces.BasicConfiguration{SDKKey: authKeyStr})
return ret, err
}

// Client creates a new HTTP client instance that isn't for SDK use.
func (c HTTPConfig) Client() *http.Client {
return c.SDKHTTPConfig.CreateHTTPClient()
}
Loading

0 comments on commit b7eb84d

Please sign in to comment.