Skip to content

Commit

Permalink
allow servers to provide custom logo
Browse files Browse the repository at this point in the history
  • Loading branch information
ukane-philemon committed Apr 11, 2023
1 parent c84ded8 commit 4c19c11
Show file tree
Hide file tree
Showing 19 changed files with 323 additions and 39 deletions.
35 changes: 35 additions & 0 deletions client/comms/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package comms

import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"
)

// TLSConfig prepares a *tls.Config struct using the provided cert.
func TLSConfig(URL string, cert []byte) (*tls.Config, error) {
if len(cert) == 0 {
return nil, nil
}

uri, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}

rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}

if ok := rootCAs.AppendCertsFromPEM(cert); !ok {
return nil, ErrInvalidCert
}

return &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
ServerName: uri.Hostname(),
}, nil
}
26 changes: 3 additions & 23 deletions client/comms/wsconn.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
Expand Down Expand Up @@ -165,28 +164,9 @@ func NewWsConn(cfg *WsCfg) (WsConn, error) {
return nil, fmt.Errorf("ping wait cannot be negative")
}

var tlsConfig *tls.Config
if len(cfg.Cert) > 0 {

uri, err := url.Parse(cfg.URL)
if err != nil {
return nil, fmt.Errorf("error parsing URL: %w", err)
}

rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}

if ok := rootCAs.AppendCertsFromPEM(cfg.Cert); !ok {
return nil, ErrInvalidCert
}

tlsConfig = &tls.Config{
RootCAs: rootCAs,
MinVersion: tls.VersionTLS12,
ServerName: uri.Hostname(),
}
tlsConfig, err := TLSConfig(cfg.URL, cfg.Cert)
if err != nil {
return nil, fmt.Errorf("TLSConfig error: %w", err)
}

return &wsConn{
Expand Down
117 changes: 117 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"math"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -1404,6 +1405,11 @@ type Config struct {
UnlockCoinsOnLogin bool
}

type cachedLogo struct {
lastRefreshed time.Time
logo []byte
}

// Core is the core client application. Core manages DEX connections, wallets,
// database access, match negotiation and more.
type Core struct {
Expand Down Expand Up @@ -1470,6 +1476,9 @@ type Core struct {
prices map[string]*stampedPrice
}
}

dexLogoMtx sync.Mutex
dexLogoCache map[string]*cachedLogo
}

// New is the constructor for a new Core.
Expand Down Expand Up @@ -1568,6 +1577,8 @@ func New(cfg *Config) (*Core, error) {

fiatRateSources: make(map[string]*commonRateSource),
pendingWallets: make(map[uint32]bool),

dexLogoCache: make(map[string]*cachedLogo),
}
c.mm.bots = make(map[uint64]*makerBot)
c.mm.cache.prices = make(map[string]*stampedPrice)
Expand Down Expand Up @@ -10013,3 +10024,109 @@ func (c *Core) saveDisabledRateSources() {
c.log.Errorf("Unable to save disabled fiat rate source to database: %v", err)
}
}

// DEXLogo returns a DEX's logo.
func (c *Core) DEXLogo(host string) []byte {
c.dexLogoMtx.Lock()
defer c.dexLogoMtx.Unlock()
// Check if we have cached the DEX logo.
cl, found := c.dexLogoCache[host]
if found && time.Since(cl.lastRefreshed) < 24*time.Hour {
return cl.logo
}

logoBytes := c.fetchDEXLogo(host)
c.dexLogoCache[host] = &cachedLogo{
lastRefreshed: time.Now(),
logo: logoBytes,
}
return logoBytes
}

// fetchDEXLogo downloads and returns the logo for an exchange.
func (c *Core) fetchDEXLogo(host string) []byte {
const (
funcName = "fetchDEXLogo"
dexLogoPath = "api/logo"
)

var cert []byte
c.connMtx.RLock()
dc, found := c.conns[host]
c.connMtx.RUnlock()
if found {
cert = dc.acct.cert
} else {
cert = CertStore[c.Network()][host]
}

scheme := "https"
if len(cert) == 0 {
scheme = "http"
}

var u url.URL
u.Host = host
u.Scheme = scheme
u.Path = dexLogoPath

// The *tls.Config returned will be nil and no error if cert is empty.
tlsConfig, err := comms.TLSConfig(u.String(), cert)
if err != nil {
c.log.Errorf("%s: comms.TLSConfig error: %v", funcName, err)
return nil
}

transport := &http.Transport{
TLSClientConfig: tlsConfig,
}

isOnionHost := isOnionHost(host)
if scheme == "http" && !isOnionHost {
c.log.Errorf("%s: a TLS connection is required when not using a hidden service", funcName)
return nil
}

if isOnionHost && c.cfg.Onion == "" {
c.log.Errorf("%s: or must be configured for .onion addresses", funcName)
return nil
}

if isOnionHost || c.cfg.TorProxy != "" {
proxyAddr := c.cfg.TorProxy
if isOnionHost {
proxyAddr = c.cfg.Onion
}

proxy := &socks.Proxy{
Addr: proxyAddr,
TorIsolation: c.cfg.TorIsolation,
}
transport.DialContext = proxy.DialContext
}

client := &http.Client{
Timeout: 5 * time.Second,
Transport: transport,
}

res, err := client.Get(u.String())
if err != nil {
c.log.Errorf("%s: http.Client.Get error %v", funcName, err)
return nil
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
c.log.Errorf("%s: received unexpected response: %s", funcName, res.Status)
return nil
}

buf := new(bytes.Buffer)
if _, err = buf.ReadFrom(res.Body); err != nil {
c.log.Errorf("%s: error reading response body: %v", funcName, err)
return nil
}

return buf.Bytes()
}
39 changes: 39 additions & 0 deletions client/webserver/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,42 @@ func (s *WebServer) orderReader(ord *core.Order) *core.OrderReader {
QuoteFeeAssetSymbol: quoteFeeAssetSymbol,
}
}

// handleGetDexLogo is the handler for the '/dexlogo/{host}' request. It
// downloads and returns the logo for an exchange.
func (s *WebServer) handleGetDexLogo(w http.ResponseWriter, r *http.Request) {
var host string
var err error

// Return a dummy logo if anything went wrong.
serveDummyLogo := func() {
http.Redirect(w, r, dummyLogoURL(host), http.StatusSeeOther)
}

if host, err = getHostCtx(r); err != nil {
serveDummyLogo()
return
}

logoBytes := s.core.DEXLogo(host)
if len(logoBytes) == 0 {
serveDummyLogo()
return
}

// Validate logo image type and use a fallback if the image is not an
// expected file type.
logoType := http.DetectContentType(logoBytes)
switch logoType {
case "image/jpeg", "image/png", "image/jpg":
default:
serveDummyLogo()
return
}

// Respond with dex logo.
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", logoType)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(logoBytes)))
w.Write(logoBytes)
}
3 changes: 3 additions & 0 deletions client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,9 @@ func (c *TCore) AddWalletPeer(assetID uint32, address string) error {
func (c *TCore) RemoveWalletPeer(assetID uint32, address string) error {
return nil
}
func (c *TCore) DEXLogo(host string) []byte {
return nil
}

var botCounter uint64

Expand Down
13 changes: 13 additions & 0 deletions client/webserver/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"

"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/order"
Expand Down Expand Up @@ -188,3 +189,15 @@ func getOrderIDCtx(r *http.Request) (dex.Bytes, error) {
}
return oidB, nil
}

// CacheControl creates a new middleware to set the HTTP response header with
// "Cache-Control: max-age=maxAge" where maxAge is in seconds.
func CacheControl(maxAge int64) func(http.Handler) http.Handler {
cacheCtrl := fmt.Sprintf("max-age=%d" + strconv.FormatInt(maxAge, 10))
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", cacheCtrl)
next.ServeHTTP(w, r)
})
}
}
5 changes: 4 additions & 1 deletion client/webserver/site/src/html/dexsettings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
{{$passwordIsCached := .UserInfo.PasswordIsCached}}
<div id="main" data-handler="dexsettings" data-host="{{.Exchange.Host}}" class="text-center py-5 overflow-y-auto">
<span class="settings-gear ico-settings"></span>
<div class="flex-center fs28 text-break">{{.Exchange.Host}}</div>
<div class="flex-center fs28 text-break">
<img class="mini-icon mt-1 mx-1 mb-0" src="/dexlogo/{{.Exchange.Host}}">
{{.Exchange.Host}}
</div>
<div class="flex-center fs16 mb-2">
<span class="me-2 connection ico-connection d-hide" id="connectedIcon"></span>
<span class="me-2 errcolor ico-disconnected d-hide" id="disconnectedIcon"></span>
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/forms.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@
</div>
<div class="d-flex flex-column align-items-stretch mt-1" data-tmpl="knownXCs">
{{range .KnownExchanges}}
<div class="known-exchange" data-host="{{.}}"><img class="micro-icon me-1" src={{dummyExchangeLogo .}}> {{.}}</div>
<div class="known-exchange" data-host="{{.}}"><img class="micro-icon me-1" src="/dexlogo/{{.}}"> {{.}}</div>
{{end}}
</div>
<div class="fs14" data-tmpl="skipRegistrationBox">
Expand Down
11 changes: 10 additions & 1 deletion client/webserver/site/src/html/settings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@
<div id="exchanges" {{if eq (len .UserInfo.Exchanges) 0}} class="d-hide"{{end}}>
<h5>[[[registered dexes]]]</h5>
{{range $host, $xc := .UserInfo.Exchanges}}
<a href="/dexsettings/{{$host}}"><button class="bg2 selected"><div class=text-break>{{$host}}<span class="dex-settings-icon ico-settings"></span></div></button></a>
<a href="/dexsettings/{{$host}}">
<button class="bg2 selected">
<div class=text-break>
<img class="micro-icon m-1" src="/dexlogo/{{$host}}">
{{$host}}
<span class="dex-settings-icon ico-settings">
</span>
</div>
</button>
</a>
{{end}}
</div>
<br>
Expand Down
21 changes: 11 additions & 10 deletions client/webserver/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,15 @@ var templateFuncs = template.FuncMap{
"x100": func(v float32) float32 {
return v * 100
},
"dummyExchangeLogo": func(host string) string {
if len(host) == 0 {
return "/img/coins/z.png"
}
char := host[0]
if char < 97 || char > 122 {
return "/img/coins/z.png"
}
return "/img/coins/" + string(char) + ".png"
},
}

func dummyLogoURL(host string) string {
if len(host) == 0 {
return "/img/coins/z.png"
}
char := host[0]
if char < 97 || char > 122 {
return "/img/coins/z.png"
}
return "/img/coins/" + string(char) + ".png"
}
2 changes: 2 additions & 0 deletions client/webserver/webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ type clientCore interface {
AddWalletPeer(assetID uint32, addr string) error
RemoveWalletPeer(assetID uint32, addr string) error
Notifications(n int) ([]*db.Notification, error)
DEXLogo(host string) []byte
}

var _ clientCore = (*core.Core)(nil)
Expand Down Expand Up @@ -357,6 +358,7 @@ func New(cfg *Config) (*WebServer, error) {
webAuth.Get(homeRoute, s.handleHome)
webAuth.Get(marketsRoute, s.handleMarkets)
webAuth.With(dexHostCtx).Get("/dexsettings/{host}", s.handleDexSettings)
webAuth.With(CacheControl(86400 /* cache for a day */), dexHostCtx).Get("/dexlogo/{host}", s.handleGetDexLogo)
if s.experimental {
webAuth.Get(marketMakerRoute, s.handleMarketMaker)
}
Expand Down
4 changes: 3 additions & 1 deletion client/webserver/webserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,9 @@ func (c *TCore) RemoveWalletPeer(assetID uint32, address string) error {
func (c *TCore) Notifications(n int) ([]*db.Notification, error) {
return c.notes, c.notesErr
}

func (c *TCore) DEXLogo(host string) []byte {
return nil
}
func (c *TCore) CreateBot(pw []byte, botType string, pgm *core.MakerProgram) (uint64, error) {
return 1, nil
}
Expand Down
Loading

0 comments on commit 4c19c11

Please sign in to comment.