Skip to content
This repository has been archived by the owner on Jul 12, 2023. It is now read-only.

use redirect hostname to lookup realm for correct mobile app #782

Merged
merged 3 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion cmd/enx-redirect/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func realMain(ctx context.Context) error {
wk := r.PathPrefix("/.well-known").Subrouter()

// Enable the iOS and Android redirect handler.
assocHandler, err := associated.New(ctx, db, cacher, h)
assocHandler, err := associated.New(ctx, cfg, db, cacher, h)
if err != nil {
return fmt.Errorf("failed to create associated links handler %w", err)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/redirect_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"fmt"
"strings"
"time"

"github.com/google/exposure-notifications-server/pkg/observability"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
Expand All @@ -36,6 +37,8 @@ type RedirectConfig struct {

AssetsPath string `env:"ASSETS_PATH, default=./cmd/enx-redirect/assets"`

AppCacheTTL time.Duration `env:"APP_CACHE_TTL, default=5m"`

// If Dev mode is true, extended logging is enabled and template
// auto-reload is enabled.
DevMode bool `env:"DEV_MODE"`
Expand Down
13 changes: 11 additions & 2 deletions pkg/controller/associated/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,17 @@ type Target struct {
}

// getAndroidData finds all the android data apps.
func (c *Controller) getAndroidData() ([]AndroidData, error) {
apps, err := c.db.ListActiveAppsByOS(database.OSTypeAndroid)
func (c *Controller) getAndroidData(region string) ([]AndroidData, error) {
realm, err := c.db.FindRealmByRegion(region)
if err != nil {
return nil, fmt.Errorf("unable to lookup realm")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("unable to lookup realm")
return nil, fmt.Errorf("unable to lookup realm: %w", err)

}
if realm == nil {
// no realm exists for this region
return nil, nil
}

apps, err := c.db.ListActiveAppsByOS(realm.ID, database.OSTypeAndroid)
if err != nil {
return nil, fmt.Errorf("failed to get android data: %w", err)
}
Expand Down
80 changes: 61 additions & 19 deletions pkg/controller/associated/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,70 +23,112 @@ package associated

import (
"context"
"fmt"
"net/http"
"time"
"strings"

"github.com/google/exposure-notifications-server/pkg/logging"
"github.com/google/exposure-notifications-verification-server/pkg/api"
"github.com/google/exposure-notifications-verification-server/pkg/cache"
"github.com/google/exposure-notifications-verification-server/pkg/config"
"github.com/google/exposure-notifications-verification-server/pkg/controller"
"github.com/google/exposure-notifications-verification-server/pkg/database"
"github.com/google/exposure-notifications-verification-server/pkg/render"
"go.uber.org/zap"
)

type Controller struct {
cacher cache.Cacher
db *database.Database
h *render.Renderer
logger *zap.SugaredLogger
config *config.RedirectConfig
hostnameToRegion map[string]string
cacher cache.Cacher
db *database.Database
h *render.Renderer
logger *zap.SugaredLogger
}

// cacheTTL is the amount of time for which to cache these values.
const cacheTTL = 30 * time.Minute
func (c *Controller) getRegion(r *http.Request) string {
// Get the hostname first
host := strings.ToLower(r.Host)
if i := strings.Index(host, ":"); i > 0 {
host = host[0:i]
}

// return the mapped region code (or default, "", if not found)
return c.hostnameToRegion[host]
}

func (c *Controller) HandleIos() http.Handler {
notFound := api.Errorf("not found")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
cacheKey := r.URL.String()
region := c.getRegion(r)
if region == "" {
c.h.RenderJSON(w, http.StatusNotFound, notFound)
return
}

var iosData IOSData
if err := c.cacher.Fetch(ctx, cacheKey, &iosData, cacheTTL, func() (interface{}, error) {
cacheKey := fmt.Sprintf("iosapps:by_region:%s", region)
var iosData *IOSData
if err := c.cacher.Fetch(ctx, cacheKey, &iosData, c.config.AppCacheTTL, func() (interface{}, error) {
c.logger.Debug("fetching new ios data")
return c.getIosData()
return c.getIosData(region)
}); err != nil {
controller.InternalError(w, r, c.h, err)
return
}

if iosData == nil {
c.h.RenderJSON(w, http.StatusNotFound, notFound)
return
}

c.h.RenderJSON(w, http.StatusOK, iosData)
})
}

func (c *Controller) HandleAndroid() http.Handler {
notFound := api.Errorf("not found")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
cacheKey := r.URL.String()
region := c.getRegion(r)
if region == "" {
c.h.RenderJSON(w, http.StatusNotFound, notFound)
return
}

cacheKey := fmt.Sprintf("androidapps:by_region:%s", region)
var androidData []AndroidData
if err := c.cacher.Fetch(ctx, cacheKey, &androidData, cacheTTL, func() (interface{}, error) {
if err := c.cacher.Fetch(ctx, cacheKey, &androidData, c.config.AppCacheTTL, func() (interface{}, error) {
c.logger.Debug("fetching new android data")
return c.getAndroidData()
return c.getAndroidData(region)
}); err != nil {
controller.InternalError(w, r, c.h, err)
return
}

if len(androidData) == 0 {
c.h.RenderJSON(w, http.StatusNotFound, notFound)
return
}

c.h.RenderJSON(w, http.StatusOK, androidData)
})
}

func New(ctx context.Context, db *database.Database, cacher cache.Cacher, h *render.Renderer) (*Controller, error) {
func New(ctx context.Context, config *config.RedirectConfig, db *database.Database, cacher cache.Cacher, h *render.Renderer) (*Controller, error) {
logger := logging.FromContext(ctx).Named("associated")

cfgMap, err := config.HostnameToRegion()
if err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}

return &Controller{
db: db,
cacher: cacher,
h: h,
logger: logger,
config: config,
db: db,
cacher: cacher,
h: h,
logger: logger,
hostnameToRegion: cfgMap,
}, nil
}
28 changes: 21 additions & 7 deletions pkg/controller/associated/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ package associated
// The iOS format is specified by:
// https://developer.apple.com/documentation/safariservices/supporting_associated_domains

import "github.com/google/exposure-notifications-verification-server/pkg/database"
import (
"fmt"

"github.com/google/exposure-notifications-verification-server/pkg/database"
)

type IOSData struct {
Applinks Applinks `json:"applinks"`
Expand Down Expand Up @@ -49,8 +53,8 @@ type Appstrings struct {
}

// getAppIds finds all the iOS app ids we know about.
func (c *Controller) getAppIds() ([]string, error) {
apps, err := c.db.ListActiveAppsByOS(database.OSTypeIOS)
func (c *Controller) getAppIds(realmID uint) ([]string, error) {
apps, err := c.db.ListActiveAppsByOS(realmID, database.OSTypeIOS)
if err != nil {
return nil, err
}
Expand All @@ -62,15 +66,25 @@ func (c *Controller) getAppIds() ([]string, error) {
}

// getIosData gets the iOS app data.
func (c *Controller) getIosData() (*IOSData, error) {
var ids []string
var err error
func (c *Controller) getIosData(region string) (*IOSData, error) {
realm, err := c.db.FindRealmByRegion(region)
if err != nil {
return nil, fmt.Errorf("unable to lookup realm")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("unable to lookup realm")
return nil, fmt.Errorf("unable to lookup realm: %w", err)

}
if realm == nil {
// no realm exists for this region
return nil, nil
}

ids, err = c.getAppIds()
ids, err := c.getAppIds(realm.ID)
if err != nil {
return nil, err
}

if len(ids) == 0 {
return nil, nil
}

return &IOSData{
Applinks: Applinks{
Details: []Detail{{
Expand Down
4 changes: 2 additions & 2 deletions pkg/database/mobile_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ func (a *MobileApp) BeforeSave(tx *gorm.DB) error {
}

// ListActiveAppsByOS finds all mobile apps by their OS.
func (db *Database) ListActiveAppsByOS(os OSType) ([]*MobileApp, error) {
func (db *Database) ListActiveAppsByOS(realmID uint, os OSType) ([]*MobileApp, error) {
mikehelmick marked this conversation as resolved.
Show resolved Hide resolved
// Find the apps.
var apps []*MobileApp
if err := db.db.
Model(&MobileApp{}).
Where("os = ?", os).
Where("realm_id = ? AND os = ?", realmID, os).
Find(&apps).
Error; err != nil {
if IsNotFound(err) {
Expand Down
12 changes: 12 additions & 0 deletions pkg/database/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,18 @@ func (db *Database) CreateRealm(name string) (*Realm, error) {
return realm, nil
}

func (db *Database) FindRealmByRegion(region string) (*Realm, error) {
mikehelmick marked this conversation as resolved.
Show resolved Hide resolved
var realm Realm

if err := db.db.Where("region_code = ?", strings.ToUpper(region)).First(&realm).Error; err != nil {
if IsNotFound(err) {
return nil, nil
}
return nil, err
}
return &realm, nil
}

func (db *Database) FindRealmByName(name string) (*Realm, error) {
var realm Realm

Expand Down