mirror of
https://github.com/dokku/dokku.git
synced 2025-12-29 00:25:08 +01:00
This plugin is mostly compatible with the nginx plugin, but runs the proxy within a docker container. Users do not have direct access to add custom openresty configuration at this time, but instead receive the ability to setup automatic ssl on first request via letsencrypt integration.
419 lines
9.0 KiB
Go
419 lines
9.0 KiB
Go
package ports
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/dokku/dokku/plugins/common"
|
|
"github.com/dokku/dokku/plugins/config"
|
|
"github.com/ryanuber/columnize"
|
|
)
|
|
|
|
func addPortMaps(appName string, portMaps []PortMap) error {
|
|
allPortMaps := getPortMaps(appName)
|
|
allPortMaps = append(allPortMaps, portMaps...)
|
|
|
|
return setPortMaps(appName, allPortMaps)
|
|
}
|
|
|
|
func clearPorts(appName string) error {
|
|
if err := common.PropertyDelete("ports", appName, "map"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return common.PropertyDelete("ports", appName, "map-detected")
|
|
}
|
|
|
|
func doesCertExist(appName string) bool {
|
|
certsExists, _ := common.PlugnTriggerOutputAsString("certs-exists", []string{appName}...)
|
|
if certsExists == "true" {
|
|
return true
|
|
}
|
|
|
|
certsForce, _ := common.PlugnTriggerOutputAsString("certs-force", []string{appName}...)
|
|
return certsForce == "true"
|
|
}
|
|
|
|
func filterAppPortMaps(appName string, scheme string, hostPort int) []PortMap {
|
|
var filteredPortMaps []PortMap
|
|
for _, portMap := range getPortMaps(appName) {
|
|
if portMap.Scheme == scheme && portMap.HostPort == hostPort {
|
|
filteredPortMaps = append(filteredPortMaps, portMap)
|
|
}
|
|
}
|
|
|
|
return filteredPortMaps
|
|
}
|
|
|
|
func getAvailablePort() int {
|
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
for {
|
|
l, err := net.ListenTCP("tcp", addr)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer l.Close()
|
|
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
if port >= 1025 && port <= 65535 {
|
|
return port
|
|
}
|
|
}
|
|
}
|
|
|
|
func getComputedProxyPort(appName string) int {
|
|
port := getProxyPort(appName)
|
|
if port == 0 {
|
|
port = getGlobalProxyPort()
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getComputedProxySSLPort(appName string) int {
|
|
port := getProxySSLPort(appName)
|
|
if port == 0 {
|
|
port = getGlobalProxySSLPort()
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getDetectedPortMaps(appName string) []PortMap {
|
|
basePort := getComputedProxyPort(appName)
|
|
if basePort == 0 {
|
|
basePort = 80
|
|
}
|
|
defaultMapping := []PortMap{
|
|
{
|
|
ContainerPort: 5000,
|
|
HostPort: basePort,
|
|
Scheme: "http",
|
|
},
|
|
}
|
|
|
|
portMaps := []PortMap{}
|
|
value, err := common.PropertyListGet("ports", appName, "map-detected")
|
|
if err == nil {
|
|
portMaps, _ = parsePortMapString(strings.Join(value, " "))
|
|
}
|
|
|
|
if len(portMaps) == 0 {
|
|
portMaps = defaultMapping
|
|
}
|
|
|
|
if doesCertExist(appName) {
|
|
setSSLPort := false
|
|
baseSSLPort := getComputedProxySSLPort(appName)
|
|
if baseSSLPort == 0 {
|
|
baseSSLPort = 443
|
|
}
|
|
|
|
for _, portMap := range portMaps {
|
|
if portMap.Scheme != "http" || portMap.HostPort != 80 {
|
|
continue
|
|
}
|
|
|
|
setSSLPort = true
|
|
portMaps = append(portMaps, PortMap{
|
|
ContainerPort: portMap.ContainerPort,
|
|
HostPort: baseSSLPort,
|
|
Scheme: "https",
|
|
})
|
|
}
|
|
|
|
if !setSSLPort {
|
|
for i, portMap := range portMaps {
|
|
if portMap.Scheme != "http" {
|
|
continue
|
|
}
|
|
|
|
portMaps[i].Scheme = "https"
|
|
}
|
|
}
|
|
}
|
|
|
|
return portMaps
|
|
}
|
|
|
|
func getGlobalProxyPort() int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_PROXY_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getGlobalProxySSLPort() int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get-global", []string{"DOKKU_PROXY_SSL_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getPortMaps(appName string) []PortMap {
|
|
value, err := common.PropertyListGet("ports", appName, "map")
|
|
if err != nil {
|
|
return []PortMap{}
|
|
}
|
|
|
|
portMaps, _ := parsePortMapString(strings.Join(value, " "))
|
|
return portMaps
|
|
}
|
|
|
|
func getProxyPort(appName string) int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_PROXY_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func getProxySSLPort(appName string) int {
|
|
port := 0
|
|
b, _ := common.PlugnTriggerOutput("config-get", []string{appName, "DOKKU_PROXY_SSL_PORT"}...)
|
|
if intVar, err := strconv.Atoi(strings.TrimSpace(string(b[:]))); err == nil {
|
|
port = intVar
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
func initializeProxyPort(appName string) error {
|
|
port := getProxyPort(appName)
|
|
if port != 0 {
|
|
return nil
|
|
}
|
|
|
|
if isAppVhostEnabled(appName) {
|
|
port = getGlobalProxyPort()
|
|
} else {
|
|
common.LogInfo1("No port set, setting to random open high port")
|
|
port = getAvailablePort()
|
|
common.LogInfo1(fmt.Sprintf("Random port %d", port))
|
|
}
|
|
|
|
if port == 0 {
|
|
port = 80
|
|
}
|
|
|
|
if err := setProxyPort(appName, port); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initializeProxySSLPort(appName string) error {
|
|
port := getProxySSLPort(appName)
|
|
if port != 0 {
|
|
return nil
|
|
}
|
|
|
|
if !doesCertExist(appName) {
|
|
return nil
|
|
}
|
|
|
|
port = getGlobalProxySSLPort()
|
|
if port == 0 {
|
|
port = 443
|
|
}
|
|
|
|
if !isAppVhostEnabled(appName) {
|
|
common.LogInfo1("No ssl port set, setting to random open high port")
|
|
port = getAvailablePort()
|
|
}
|
|
|
|
if err := setProxySSLPort(appName, port); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func inRange(value int, min int, max int) bool {
|
|
return min < value && value < max
|
|
}
|
|
|
|
func isAppVhostEnabled(appName string) bool {
|
|
if err := common.PlugnTrigger("domains-vhost-enabled", []string{appName}...); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func listAppPortMaps(appName string) error {
|
|
portMaps := getPortMaps(appName)
|
|
|
|
if len(portMaps) == 0 {
|
|
return errors.New("No port mappings configured for app")
|
|
}
|
|
|
|
var lines []string
|
|
if os.Getenv("DOKKU_QUIET_OUTPUT") == "" {
|
|
lines = append(lines, "-----> scheme:host port:container port")
|
|
}
|
|
|
|
for _, portMap := range portMaps {
|
|
lines = append(lines, portMap.String())
|
|
}
|
|
|
|
sort.Strings(lines)
|
|
common.LogInfo1Quiet(fmt.Sprintf("Port mappings for %s", appName))
|
|
config := columnize.DefaultConfig()
|
|
config.Delim = ":"
|
|
config.Prefix = " "
|
|
config.Empty = ""
|
|
fmt.Println(columnize.Format(lines, config))
|
|
return nil
|
|
}
|
|
|
|
func parsePortMapString(stringPortMap string) ([]PortMap, error) {
|
|
var portMaps []PortMap
|
|
|
|
for _, v := range strings.Split(strings.TrimSpace(stringPortMap), " ") {
|
|
parts := strings.SplitN(v, ":", 3)
|
|
if len(parts) == 1 {
|
|
hostPort, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
if !inRange(hostPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [hostPort=%d]", v, hostPort)
|
|
}
|
|
|
|
portMaps = append(portMaps, PortMap{
|
|
HostPort: hostPort,
|
|
Scheme: "__internal__",
|
|
})
|
|
continue
|
|
}
|
|
|
|
if len(parts) != 3 {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [len=%d]", v, len(parts))
|
|
}
|
|
|
|
hostPort, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
containerPort, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [err=%s]", v, err.Error())
|
|
}
|
|
|
|
if !inRange(hostPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [hostPort=%d]", v, hostPort)
|
|
}
|
|
|
|
if !inRange(containerPort, 0, 65536) {
|
|
return portMaps, fmt.Errorf("Invalid port map %s [containerPort=%d]", v, containerPort)
|
|
}
|
|
|
|
portMaps = append(portMaps, PortMap{
|
|
ContainerPort: containerPort,
|
|
HostPort: hostPort,
|
|
Scheme: parts[0],
|
|
})
|
|
}
|
|
|
|
return uniquePortMaps(portMaps), nil
|
|
}
|
|
|
|
func removePortMaps(appName string, portMaps []PortMap) error {
|
|
toRemove := map[string]bool{}
|
|
toRemoveByPort := map[int]bool{}
|
|
|
|
for _, portMap := range portMaps {
|
|
if portMap.AllowsPersistence() {
|
|
toRemoveByPort[portMap.HostPort] = true
|
|
continue
|
|
}
|
|
toRemove[portMap.String()] = true
|
|
}
|
|
|
|
var toSet []PortMap
|
|
for _, portMap := range getPortMaps(appName) {
|
|
if toRemove[portMap.String()] {
|
|
continue
|
|
}
|
|
|
|
if toRemoveByPort[portMap.HostPort] {
|
|
continue
|
|
}
|
|
|
|
toSet = append(toSet, portMap)
|
|
}
|
|
|
|
if len(toSet) == 0 {
|
|
return common.PropertyDelete("ports", appName, "map")
|
|
}
|
|
|
|
return setPortMaps(appName, toSet)
|
|
}
|
|
|
|
func setPortMaps(appName string, portMaps []PortMap) error {
|
|
var value []string
|
|
for _, portMap := range uniquePortMaps(portMaps) {
|
|
if portMap.AllowsPersistence() {
|
|
continue
|
|
}
|
|
|
|
value = append(value, portMap.String())
|
|
}
|
|
|
|
sort.Strings(value)
|
|
return common.PropertyListWrite("ports", appName, "map", value)
|
|
}
|
|
|
|
func setProxyPort(appName string, port int) error {
|
|
return common.EnvWrap(func() error {
|
|
entries := map[string]string{
|
|
"DOKKU_PROXY_PORT": fmt.Sprint(port),
|
|
}
|
|
return config.SetMany(appName, entries, false)
|
|
}, map[string]string{"DOKKU_QUIET_OUTPUT": "1"})
|
|
}
|
|
|
|
func setProxySSLPort(appName string, port int) error {
|
|
return common.EnvWrap(func() error {
|
|
entries := map[string]string{
|
|
"DOKKU_PROXY_SSL_PORT": fmt.Sprint(port),
|
|
}
|
|
return config.SetMany(appName, entries, false)
|
|
}, map[string]string{"DOKKU_QUIET_OUTPUT": "1"})
|
|
}
|
|
|
|
func uniquePortMaps(portMaps []PortMap) []PortMap {
|
|
var unique []PortMap
|
|
existingPortMaps := map[string]bool{}
|
|
|
|
for _, portMap := range portMaps {
|
|
if existingPortMaps[portMap.String()] {
|
|
continue
|
|
}
|
|
|
|
existingPortMaps[portMap.String()] = true
|
|
unique = append(unique, portMap)
|
|
}
|
|
|
|
return unique
|
|
}
|