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

Docker Compatibility #45

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 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
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,52 @@ To proper use this software, here is the checklist:

Now simply run it and add a shop inside tinfoil with the address setup in `config` (or `http://localIp:3000` if not specified).

# 🐋 Docker
Copy link
Owner

Choose a reason for hiding this comment

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

I would add this section after Dev or build from source and maybe add a sentence with link and this end of 🎮 Use to talk about how to use it with docker.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved the section below Dev or build from source but I don't understand the 🎮 Use part.


To run with [Docker](https://docs.docker.com/engine/install/), you can use this as a starting `cli` example:

`docker run -d --restart=always -e TINSHOP_SOURCES_DIRECTORIES=/games -e TINSHOP_WELCOMEMESSAGE="Welcome to my Tinshop!" -v /local/game/backups:/games -p 3000:3000`

This will run Tinshop on `http://localhost:3000` and persist across reboots!

If `docker compose` is your thing, then start with this example:

```yaml
version: '3.9'
services:
tinshop:
container_name: tinshop
image: helvio/tinshop:latest
restart: always
ports:
- 3000:3000
environment:
- TINSHOP_SOURCES_DIRECTORIES=/games
- TINSHOP_WELCOMEMESSAGE=Welcome to my Tinshop!
volumes:
- /media/switch:/games
```
All of the settings in the `config.yaml` file are valid Environment Variables. They must be `UPPERCASE` and prefixed by `TINSHOP_`. Nested properties should be prefixed by `_`. Here are a few examples:

| ENV_VAR | `config.yaml` entry | Default Value | Example Value |
|------------------------------|---------------------|--------------------------------|-----------------------------------|
| TINSHOP_HOST | host | `0.0.0.0` | `127.0.0.` |
| TINSHOP_PROTOCOL | protocol | `http` | `https` |
| TINSHOP_NAME | name | `TinShop` | `MyShop` |
| TINSHOP_REVERSEPROXY | reverseProxy | `false` | `true` |
| TINSHOP_WELCOMEMESSAGE | welcomeMessage | `Welcome to your own TinShop!` | `Welcome to my shop!` |
| TINSHOP_NOWELCOMEMESSAGE | noWelcomeMessage | `false` | `true` |
| TINSHOP_DEBUG_NFS | debug.nfs | `false` | `true` |
| TINSHOP_DEBUG_NOSECURITY | debug.nosecurity | `false` | `true` |
| TINSHOP_DEBUG_TICKET | debug.ticket | `false` | `true` |
| TINSHOP_NSP_CHECKVERIFIED | nsp.checkVerified | `true` | `false` |
| TINSHOP_SOURCES_DIRECTORIES | sources.directories | `./games` | `/games /path/two /path/three` |
| TINSHOP_SOURCES_NSF | sources.nfs | `null` | `192.168.1.100:/path/to/games` |
| TINSHOP_SECURITY_BANNEDTHEME | sources.bannedTheme | `null` | `THEME1 THEME2 THEME3` |
| TINSHOP_SECURITY_WHITELIST | sources.whitelist | `null` | `NSWID1 NSWID2 NSWID3` |
| TINSHOP_SECURITY_BLACKLIST | sources.blacklist | `null` | `NSWID4 NSWID5 NSWID6` |
| TINSHOP_SECURITY_FORWARDAUTH | sources.forwardAuth | `null` | `https://auth.tinshop.com/switch` |

# 🎉 Features

Here is the list of all main features so far:
Expand Down Expand Up @@ -148,6 +194,37 @@ reverseProxy: true
```

If you want to have HTTPS, ensure `caddy` handle it (it will with Let's Encrypt) and change `https` in the config and remove `:80` in the `Caddyfile` example.

### Example for traefik

To work with [`traefik`](https://traefik.io/), you need to put in your Dynamic Configuration something similar to this:

```yaml
http:
routers:
service: tinshop
rule: Host(`tinshop.example.com`)
entryPoints: websecure # Could be web if not using https

services:
tinshop:
loadBalancer:
servers:
- url: http://192.168.1.2:3000
```

and your `config.yaml` as follow:

```yaml
host: tinshop.example.com
protocol: http
port: 3000
reverseProxy: true
```

If you want to have HTTPS, ensure `traefik` handle it (it will with Let's Encrypt) and change `https` in the config and remove `:80` in the `Caddyfile` example.
Helvio88 marked this conversation as resolved.
Show resolved Hide resolved

For more details on Traefik + Let's Encrypt, [click here](https://doc.traefik.io/traefik/https/acme/).
</details>

## How can I add a `basic auth` to protect my shop?
Expand Down
104 changes: 64 additions & 40 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net"
"os"
"strconv"
"strings"

"github.com/DblK/tinshop/repository"
"github.com/DblK/tinshop/utils"
Expand All @@ -35,8 +36,8 @@ type nsp struct {
CheckVerified bool `mapstructure:"checkVerified"`
}

// File holds all config information
type File struct {
// Configuration holds all config information
type Configuration struct {
rootShop string
ShopHost string `mapstructure:"host"`
ShopProtocol string `mapstructure:"protocol"`
Expand All @@ -58,18 +59,41 @@ type File struct {

// New returns a new configuration
func New() repository.Config {
return &File{}
return &Configuration{}
}

// LoadConfig handles viper under the hood
func (cfg *File) LoadConfig() {
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.SetDefault("sources.directories", "./games")
func (cfg *Configuration) LoadConfig() {
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath(".") // optionally look for config in the working directory
viper.SetTypeByDefaultValue(true) // Allows []string to be parsed from Env Vars

viper.SetDefault("host", "tinshop.example.com")
Helvio88 marked this conversation as resolved.
Show resolved Hide resolved
viper.SetDefault("protocol", "http")
Copy link
Owner

Choose a reason for hiding this comment

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

Is function ComputeDefaultValues, http is assumed if no protocol, so default value is not needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is a matter of preference, so I will make my case but accept your decision.
viper needs a default set (could be nil) in order to assign ENV vars to configuration keys.
For the sake of standardization, I believe this could stay here, and the ComputeDefaultValues should test for invalid values and force them to be the default, instead of being the default section “setter”. This way, all the default values are in one place. What are your thoughts?

viper.SetDefault("name", "TinShop")
viper.SetDefault("reverseProxy", false)
viper.SetDefault("welcomeMessage", "Welcome to your own TinShop!")
viper.SetDefault("noWelcomeMessage", false)

viper.SetDefault("debug.nfs", false)
Helvio88 marked this conversation as resolved.
Show resolved Hide resolved
viper.SetDefault("debug.noSecurity", false)
viper.SetDefault("debug.ticket", false)

viper.SetDefault("nsp.checkVerified", true)
Helvio88 marked this conversation as resolved.
Show resolved Hide resolved

viper.SetDefault("sources.directories", []string{"./games"})
viper.SetDefault("sources.nfs", []string{})

viper.SetDefault("security.bannedTheme", []string{})
viper.SetDefault("security.whitelist", []string{})
viper.SetDefault("security.blacklist", []string{})
viper.SetDefault("security.forwardAuth", "")

viper.SetEnvPrefix("TINSHOP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
Expand All @@ -89,7 +113,7 @@ func (cfg *File) LoadConfig() {
cfg.configChange()
}

func (cfg *File) configChange() {
func (cfg *Configuration) configChange() {
// Call all before hooks
for _, hook := range cfg.beforeAllHooks {
hook(cfg)
Expand Down Expand Up @@ -121,8 +145,8 @@ func (cfg *File) configChange() {
}
}

func loadAndCompute() *File {
var loadedConfig = &File{}
func loadAndCompute() *Configuration {
var loadedConfig = &Configuration{}
err := viper.Unmarshal(&loadedConfig)

if err != nil {
Expand Down Expand Up @@ -169,7 +193,7 @@ func ComputeDefaultValues(config repository.Config) repository.Config {
rootShop += ":" + strconv.Itoa(config.Port())
}
}
log.Println((rootShop))
log.Println(rootShop)
config.SetRootShop(rootShop)

config.SetShopTemplateData(repository.ShopTemplate{
Expand All @@ -180,153 +204,153 @@ func ComputeDefaultValues(config repository.Config) repository.Config {
}

// AddHook Add hook function on change config
func (cfg *File) AddHook(f func(repository.Config)) {
func (cfg *Configuration) AddHook(f func(repository.Config)) {
cfg.allHooks = append(cfg.allHooks, f)
}

// AddBeforeHook Add hook function before on change config
func (cfg *File) AddBeforeHook(f func(repository.Config)) {
func (cfg *Configuration) AddBeforeHook(f func(repository.Config)) {
cfg.beforeAllHooks = append(cfg.beforeAllHooks, f)
}

// SetRootShop allow to change the root url of the shop
func (cfg *File) SetRootShop(root string) {
func (cfg *Configuration) SetRootShop(root string) {
cfg.rootShop = root
}

// RootShop returns the RootShop url
func (cfg *File) RootShop() string {
func (cfg *Configuration) RootShop() string {
return cfg.rootShop
}

// ReverseProxy returns the ReverseProxy setting
func (cfg *File) ReverseProxy() bool {
func (cfg *Configuration) ReverseProxy() bool {
return cfg.Proxy
}

// WelcomeMessage returns the WelcomeMessage
func (cfg *File) WelcomeMessage() string {
func (cfg *Configuration) WelcomeMessage() string {
return cfg.ShopWelcomeMessage
}

// NoWelcomeMessage returns the NoWelcomeMessage
func (cfg *File) NoWelcomeMessage() bool {
func (cfg *Configuration) NoWelcomeMessage() bool {
return cfg.ShopNoWelcomeMessage
}

// Protocol returns the protocol scheme (http or https)
func (cfg *File) Protocol() string {
func (cfg *Configuration) Protocol() string {
return cfg.ShopProtocol
}

// Host returns the host of the shop
func (cfg *File) Host() string {
func (cfg *Configuration) Host() string {
return cfg.ShopHost
}

// Port returns the port number for outside access
func (cfg *File) Port() int {
func (cfg *Configuration) Port() int {
return cfg.ShopPort
}

// DebugTicket tells if we should display additional log for ticket verification
func (cfg *File) DebugTicket() bool {
func (cfg *Configuration) DebugTicket() bool {
return cfg.Debug.Ticket
}

// DebugNfs tells if we should display additional log for nfs
func (cfg *File) DebugNfs() bool {
func (cfg *Configuration) DebugNfs() bool {
return cfg.Debug.Nfs
}

// DebugNoSecurity returns if we should disable security or not
func (cfg *File) DebugNoSecurity() bool {
func (cfg *Configuration) DebugNoSecurity() bool {
return cfg.Debug.NoSecurity
}

// Directories returns the list of directories sources
func (cfg *File) Directories() []string {
func (cfg *Configuration) Directories() []string {
return cfg.AllSources.Directories
}

// CustomDB returns the list of custom title db
func (cfg *File) CustomDB() map[string]repository.TitleDBEntry {
func (cfg *Configuration) CustomDB() map[string]repository.TitleDBEntry {
return cfg.CustomTitleDB
}

// NfsShares returns the list of nfs sources
func (cfg *File) NfsShares() []string {
func (cfg *Configuration) NfsShares() []string {
return cfg.AllSources.Nfs
}

// Sources returns all available sources
func (cfg *File) Sources() repository.ConfigSources {
func (cfg *Configuration) Sources() repository.ConfigSources {
return cfg.AllSources
}

// ShopTemplateData returns the data needed to render template
func (cfg *File) ShopTemplateData() repository.ShopTemplate {
func (cfg *Configuration) ShopTemplateData() repository.ShopTemplate {
return cfg.shopTemplateData
}

// SetShopTemplateData sets the data for template
func (cfg *File) SetShopTemplateData(data repository.ShopTemplate) {
func (cfg *Configuration) SetShopTemplateData(data repository.ShopTemplate) {
cfg.shopTemplateData = data
}

// ShopTitle returns the name of the shop
func (cfg *File) ShopTitle() string {
func (cfg *Configuration) ShopTitle() string {
return cfg.Name
}

// VerifyNSP tells if we need to verify NSP
func (cfg *File) VerifyNSP() bool {
func (cfg *Configuration) VerifyNSP() bool {
return cfg.NSP.CheckVerified
}

// ForwardAuthURL returns the url of the forward auth
func (cfg *File) ForwardAuthURL() string {
func (cfg *Configuration) ForwardAuthURL() string {
return cfg.Security.ForwardAuth
}

// IsBlacklisted tells if the uid is blacklisted or not
func (cfg *File) IsBlacklisted(uid string) bool {
func (cfg *Configuration) IsBlacklisted(uid string) bool {
if len(cfg.Security.Whitelist) != 0 {
return !cfg.isInWhiteList(uid)
}
return cfg.isInBlackList(uid)
}

// IsWhitelisted tells if the uid is whitelisted or not
func (cfg *File) IsWhitelisted(uid string) bool {
func (cfg *Configuration) IsWhitelisted(uid string) bool {
if len(cfg.Security.Whitelist) == 0 {
return !cfg.isInBlackList(uid)
}
return cfg.isInWhiteList(uid)
}

func (cfg *File) isInBlackList(uid string) bool {
func (cfg *Configuration) isInBlackList(uid string) bool {
idxBlackList := utils.Search(len(cfg.Security.Blacklist), func(index int) bool {
return cfg.Security.Blacklist[index] == uid
})
return idxBlackList != -1
}
func (cfg *File) isInWhiteList(uid string) bool {
func (cfg *Configuration) isInWhiteList(uid string) bool {
idxWhiteList := utils.Search(len(cfg.Security.Whitelist), func(index int) bool {
return cfg.Security.Whitelist[index] == uid
})
return idxWhiteList != -1
}

// IsBannedTheme tells if the theme is banned or not
func (cfg *File) IsBannedTheme(theme string) bool {
func (cfg *Configuration) IsBannedTheme(theme string) bool {
idxBannedTheme := utils.Search(len(cfg.Security.BannedTheme), func(index int) bool {
return cfg.Security.BannedTheme[index] == theme
})
return idxBannedTheme != -1
}

// BannedTheme returns all banned theme
func (cfg *File) BannedTheme() []string {
func (cfg *Configuration) BannedTheme() []string {
return cfg.Security.BannedTheme
}
Loading