Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(miniooni): add authentication to oonirun #1627

Merged
merged 14 commits into from
Jun 27, 2024
8 changes: 8 additions & 0 deletions internal/cmd/miniooni/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
// Options contains the options you can set from the CLI.
type Options struct {
Annotations []string
AuthFile string
Emoji bool
ExtraOptions []string
HomeDir string
Expand Down Expand Up @@ -228,6 +229,13 @@ func registerOONIRun(rootCmd *cobra.Command, globalOptions *Options) {
[]string{},
"Path to the OONI Run v2 descriptor to run (may be specified multiple times)",
)
flags.StringVarP(
&globalOptions.AuthFile,
"bearer-token-file",
"",
"",
"Path to a file containing a bearer token for fetching a remote OONI Run v2 descriptor",
)
}

// registerAllExperiments registers a subcommand for each experiment
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/miniooni/oonirun.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func ooniRunMain(ctx context.Context,
logger := sess.Logger()
cfg := &oonirun.LinkConfig{
AcceptChanges: currentOptions.Yes,
AuthFile: currentOptions.AuthFile,
Annotations: annotations,
KVStore: sess.KeyValueStore(),
MaxRuntime: currentOptions.MaxRuntime,
Expand Down
4 changes: 4 additions & 0 deletions internal/oonirun/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type LinkConfig struct {
// reviewing what it contains or what has changed.
AcceptChanges bool

// AuthFile is OPTIONAL and will add an authentication header to the
// request used for fetching this OONI Run link.
AuthFile string

// Annotations contains OPTIONAL Annotations for the experiment.
Annotations map[string]string

Expand Down
65 changes: 60 additions & 5 deletions internal/oonirun/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ package oonirun
//

import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"sync/atomic"

"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
"github.com/ooni/probe-cli/v3/internal/fsx"
"github.com/ooni/probe-cli/v3/internal/httpclientx"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/model"
Expand Down Expand Up @@ -63,12 +67,16 @@ type V2Nettest struct {
// getV2DescriptorFromHTTPSURL GETs a v2Descriptor instance from
// a static URL (e.g., from a GitHub repo or from a Gist).
func getV2DescriptorFromHTTPSURL(ctx context.Context, client model.HTTPClient,
logger model.Logger, URL string) (*V2Descriptor, error) {
logger model.Logger, URL, auth string) (*V2Descriptor, error) {
if auth != "" {
// we assume a bearer token
auth = fmt.Sprintf("Bearer %s", auth)
}
return httpclientx.GetJSON[*V2Descriptor](
ctx,
httpclientx.NewEndpoint(URL),
&httpclientx.Config{
Authorization: "", // not needed
Authorization: auth,
Client: client,
Logger: logger,
UserAgent: model.HTTPHeaderUserAgent,
Expand Down Expand Up @@ -140,9 +148,9 @@ func v2DescriptorCacheLoad(fsstore model.KeyValueStore) (*v2DescriptorCache, err
// - err is the error that occurred, or nil in case of success.
func (cache *v2DescriptorCache) PullChangesWithoutSideEffects(
ctx context.Context, client model.HTTPClient, logger model.Logger,
URL string) (oldValue, newValue *V2Descriptor, err error) {
URL, auth string) (oldValue, newValue *V2Descriptor, err error) {
oldValue = cache.Entries[URL]
newValue, err = getV2DescriptorFromHTTPSURL(ctx, client, logger, URL)
newValue, err = getV2DescriptorFromHTTPSURL(ctx, client, logger, URL, auth)
return
}

Expand Down Expand Up @@ -263,7 +271,11 @@ func v2MeasureHTTPS(ctx context.Context, config *LinkConfig, URL string) error {

// pull a possibly new descriptor without updating the old descriptor
clnt := config.Session.DefaultHTTPClient()
oldValue, newValue, err := cache.PullChangesWithoutSideEffects(ctx, clnt, logger, URL)
auth, err := v2MaybeGetAuthenticationTokenFromFile(config.AuthFile)
if err != nil {
logger.Warnf("oonirun: failed to retrieve auth token: %v", err)
}
oldValue, newValue, err := cache.PullChangesWithoutSideEffects(ctx, clnt, logger, URL, auth)
if err != nil {
return err
}
Expand All @@ -290,3 +302,46 @@ func v2MeasureHTTPS(ctx context.Context, config *LinkConfig, URL string) error {
// note: this function gracefully handles nil values
return V2MeasureDescriptor(ctx, config, newValue)
}

func v2MaybeGetAuthenticationTokenFromFile(path string) (string, error) {
if path != "" {
return v2ReadBearerTokenFromFile(path)
}
return "", nil
}

// v2ReadBearerTokenFromFile tries to extract a valid (base64) bearer token from
// the first line of the passed text file.
// If there is an error while reading from the file, the error will be returned.
// If we can read from the file but there's no valid token found, an empty string will be returned.
func v2ReadBearerTokenFromFile(fileName string) (string, error) {
filep, err := fsx.OpenFile(fileName)
if err != nil {
return "", err
}
defer filep.Close()

scanner := bufio.NewScanner(filep)

// Scan the first line
if scanner.Scan() {
line := scanner.Text()

token := strings.TrimSpace(line)

// if this is not a valid base64 token, return empty string
if _, err := base64.StdEncoding.DecodeString(token); err != nil {
return "", nil
}

return token, nil
}

// Check for any scanning error
if err := scanner.Err(); err != nil {
return "", err
}

// Return empty string if file is empty
return "", nil
}
Loading
Loading