Skip to content

Commit

Permalink
Merge pull request #113 from Michad/tls
Browse files Browse the repository at this point in the history
Support TLS including Let's Encrypt
  • Loading branch information
Michad authored Jul 13, 2024
2 parents 9057ade + 99c7ad6 commit 15a591b
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ disk_tile_cache/
tilegroxy
test_config.yml
.env

certs/

*.log
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Tilegroxy shines when you consume maps from multiple sources. It isn't tied to
* Generic support for any content type (raster or vector)
* Configurable timeout, logging, and error handling rules
* Commands for seeding and testing your layers
* Run as HTTPS including Let's Encrypt support
* Container deployment

The following are on the roadmap and expected before a 1.0 release:
Expand All @@ -34,7 +35,6 @@ The following are on the roadmap and expected before a 1.0 release:
* Providers that composite/modify vector layers formats such as [MVT](https://github.com/mapbox/vector-tile-spec) or tiled GeoJSON
* OpenTelemetry support
* Support for external secret stores such as AWS Secrets Manager to avoid secrets in the configuration
* Support for HTTPS server w/ Let's Encrypt or static certs



Expand Down
31 changes: 31 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ Configuration options:
| Production | bool | No | false | Hardens operation for usage in production. For instance, controls serving splash page, documentation, x-powered-by header. |
| Timeout | uint | No | 60 | How long (in seconds) a request can be in flight before we cancel it and return an error |
| Gzip | bool | No | false | Whether to gzip compress HTTP responses |
| Encrypt | [Encryption](#encryption) | No | None | Configuration for enabling TLS (HTTPS). Don't specify to operate without encryption (the default) |

The following can be supplied as environment variables:

Expand All @@ -635,6 +636,36 @@ The following can be supplied as environment variables:
| Timeout | SERVER_TIMEOUT |
| Gzip | SERVER_GZIP |

### Encryption

Configures how encryption should be applied to the server.

There are two main ways this can work:
1. With a pre-supplied certificate and keyfile
2. Via [Let's Encrypt](https://letsencrypt.org/how-it-works/) (ACME) using Go's built-in autocert module

If a certificate and keyfile are supplied the server will utilize option 1, otherwise it'll fallback to option 2. If you don't want to utilize encryption (for example you have TLS termination handled externally) simply omit `Server.Encrypt`

Configuration options:

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| Domain | string | Yes | None | The domain name you're operating with (the domain end-users use) |
| Cache | string | No | ./certs | The path to a directory to cache certificates in if using let's encrypt.
| Certificate | string | None | The file path to the TLS certificate
| KeyFile | string | None | The file path to the keyfile
| HttpPort | int | No | None |The port used for non-encrypted traffic. Required if using Let's Encrypt to provide for the ACME challenge, in which case this needs to indirectly be 80 (that is, this can be set to e.g. 8080 if something ahead of this redirects 80 to 8080). Everything except .well-known will be redirected to the main port when set. |

The following can be supplied as environment variables:

| Configuration Parameter | Environment Variable |
| --- | --- |
| Domain | SERVER_ENCRYPT_DOMAIN |
| Cache | SERVER_ENCRYPT_CACHE |
| Certificate | SERVER_ENCRYPT_CERTIFICATE |
| KeyFile | SERVER_ENCRYPT_KEYFILE |
| HttpPort | SERVER_ENCRYPT_HTTPPORT |


## Client

Expand Down
52 changes: 52 additions & 0 deletions examples/configurations/prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Server:
BindHost: 0.0.0.0
Gzip: true
Port: 8443
Production: true
Headers:
Access-Control-Allow-Origin: "*"
Timeout: 30
Encrypt:
Domain: dev.local.io
Certificate: certs/dev.local.io.crt
Keyfile: certs/dev.local.io.key
Logging:
Access:
Console: false
Format: combined
Path: "my-access.log"
Main:
Console: true
Format: json
Level: error
Path: "my-main.log"
# Authentication:
# name: jwt
# algorithm: HS256
# key: some_secret_key
Client:
UnknownLength: false
ContentTypes:
- image/png
- image/jpeg
- image/webp
StatusCodes:
- 200
- 201
MaxLength: 100000000
UserAgent: company/1.0
error:
Mode: image
cache:
name: multi
tiers:
- name: memory
maxsize: 1000
ttl: 1000
- name: disk
path: "./disk_tile_cache"
layers:
- id: osm
provider:
name: proxy
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
12 changes: 12 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ import (
_ "github.com/spf13/viper/remote"
)

// Configuration for TLS (HTTPS) operation. If this is configured then TLS is enabled. This can operate either with a static certificate and keyfile via the filesystem or via ACME/Let's Encrypt
type EncryptionConfig struct {
Domain string //The domain name you're operating with (the domain end-users use). Required
Cache string //The path to a directory to cache certificates in if using let's encrypt. Defaults to ./certs
Certificate string //The file path to get to the TLS certificate
KeyFile string //The file path to get to the keyfile
HttpPort int //The port used for non-encrypted traffic. Required if using Let's Encrypt for ACME challenge and needs to indirectly be 80 (that is, it could be 8080 if something else redirects 80 to 8080). Everything except .well-known will be redirected to the main port when set.
}

type ServerConfig struct {
Encrypt *EncryptionConfig //Whether and how to use TLS. Defaults to none AKA no encryption.
BindHost string //IP address to bind HTTP server to
Port int //Port to bind HTTP server to
RootPath string //Root HTTP Path to apply to all endpoints. Defaults to /
Expand Down Expand Up @@ -78,6 +88,7 @@ const (
// replaced with fully static constants later if it does turn out nobody ever sees value in it
type ErrorMessages struct {
NotAuthorized string
ParamRequired string
InvalidParam string
RangeError string
ServerError string
Expand Down Expand Up @@ -215,6 +226,7 @@ func DefaultConfig() Config {
ScriptError: "The script specified for %v is invalid: %v",
OneOfRequired: "You must specify one of: %v",
Timeout: "Timeout error",
ParamRequired: "Parameter %v is required",
},
Images: ErrorImages{
OutOfBounds: images.KeyImageTransparent,
Expand Down
51 changes: 49 additions & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/Michad/tilegroxy/internal/config"
"github.com/Michad/tilegroxy/internal/images"
"github.com/Michad/tilegroxy/internal/layers"
"golang.org/x/crypto/acme/autocert"

"github.com/gorilla/handlers"
)
Expand Down Expand Up @@ -265,7 +266,19 @@ func (h httpContextHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
h.Handler.ServeHTTP(w, req.WithContext(&reqC))
}

type httpRedirectHandler struct {
protoAndHost string
}

func (h httpRedirectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, h.protoAndHost+req.RequestURI, http.StatusMovedPermanently)
}

func ListenAndServe(config *config.Config, layerGroup *layers.LayerGroup, auth authentication.Authentication) error {
if config.Server.Encrypt != nil && config.Server.Encrypt.Domain == "" {
return fmt.Errorf(config.Error.Messages.ParamRequired, "server.encrypt.domain")
}

r := http.ServeMux{}

if config.Server.Production {
Expand Down Expand Up @@ -317,12 +330,46 @@ func ListenAndServe(config *config.Config, layerGroup *layers.LayerGroup, auth a
go func() {
defer func() {
if r := recover(); r != nil {
srvErr <- fmt.Errorf("unexpected server error %v", r)
srvErr <- fmt.Errorf("unexpected server error %v \n %v", r, string(debug.Stack()))
}
}()

slog.InfoContext(context.Background(), "Binding...")
srvErr <- srv.ListenAndServe()

if config.Server.Encrypt != nil {
httpPort := config.Server.Encrypt.HttpPort
httpHostPort := net.JoinHostPort(config.Server.BindHost, strconv.Itoa(httpPort))

if config.Server.Encrypt.Certificate != "" && config.Server.Encrypt.KeyFile != "" {
if httpPort != 0 {
go http.ListenAndServe(httpHostPort, httpRedirectHandler{protoAndHost: "https://" + config.Server.Encrypt.Domain})
}

srvErr <- srv.ListenAndServeTLS(config.Server.Encrypt.Certificate, config.Server.Encrypt.KeyFile)
} else {
//Let's Encrypt workflow
cacheDir := "certs"
if config.Server.Encrypt.Cache != "" {
cacheDir = config.Server.Encrypt.Cache
}

certManager := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(config.Server.Encrypt.Domain),
Cache: autocert.DirCache(cacheDir),
}

if httpPort != 0 {
go http.ListenAndServe(httpHostPort, certManager.HTTPHandler(nil))
}

srv.TLSConfig = certManager.TLSConfig()

srvErr <- srv.ListenAndServeTLS("", "")
}
} else {
srvErr <- srv.ListenAndServe()
}
}()

select {
Expand Down

0 comments on commit 15a591b

Please sign in to comment.