web.go 5.21 KB
Newer Older
1
//
Christian Pointner's avatar
Christian Pointner committed
2
3
//  tank, Import and Playlist Daemon for Aura project
//  Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at>
4
//
Christian Pointner's avatar
Christian Pointner committed
5
6
7
8
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
9
//
Christian Pointner's avatar
Christian Pointner committed
10
//  This program is distributed in the hope that it will be useful,
11
12
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
Christian Pointner's avatar
Christian Pointner committed
13
//  GNU Affero General Public License for more details.
14
//
Christian Pointner's avatar
Christian Pointner committed
15
16
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
17
18
19
20
21
//

package main

import (
22
	"context"
23
	"fmt"
24
25
	"net"
	"net/http"
26
27
	"os"
	"strings"
28
	"sync"
29
30
	"time"

Christian Pointner's avatar
Christian Pointner committed
31
	"github.com/gin-gonic/gin"
32
	cors "github.com/rs/cors/wrapper/gin"
33
34
35
	"github.com/swaggo/gin-swagger"
	"github.com/swaggo/gin-swagger/swaggerFiles"
	_ "gitlab.servus.at/autoradio/tank/api/docs"
Christian Pointner's avatar
Christian Pointner committed
36
	apiV1 "gitlab.servus.at/autoradio/tank/api/v1"
37
	"gitlab.servus.at/autoradio/tank/auth"
38
39
	"gitlab.servus.at/autoradio/tank/importer"
	"gitlab.servus.at/autoradio/tank/store"
40
	"gitlab.servus.at/autoradio/tank/ui"
41
42
43
)

const (
44
45
46
47
48
	WebUIPathPrefix  = "/ui/"
	WebAuthPrefix    = "/auth/"
	WebAPIDocsPrefix = "/api/docs/"
	WebAPIv1Prefix   = "/api/v1/"
	HealthzEndpoint  = "/healthz"
49
50
)

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
func apache2CombinedLogger(param gin.LogFormatterParams) string {
	return fmt.Sprintf("%s - - [%s] \"%s %s %s\" %d %d \"-\" \"%s\"\n",
		param.ClientIP,
		param.TimeStamp.Format(time.RFC1123),
		param.Method,
		param.Path,
		param.Request.Proto,
		param.StatusCode,
		param.BodySize,
		param.Request.UserAgent(),
	)
}

func installLogger(r *gin.Engine, conf WebConfig) error {
	if conf.AccessLogs == "" {
		return nil
	}

	var logConfig gin.LoggerConfig
	target := strings.SplitN(conf.AccessLogs, ":", 2)
	switch strings.ToLower(target[0]) {
	case "stdout":
		logConfig.Output = os.Stdout
	case "stderr":
		logConfig.Output = os.Stderr
	case "file":
		if len(target) != 2 {
			return fmt.Errorf("please specify a path to the access log")
		}
		f, err := os.OpenFile(target[1], os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			return fmt.Errorf("failed to open access log: %v", err)
		}
		logConfig.Output = f
		logConfig.Formatter = apache2CombinedLogger
	default:
		return fmt.Errorf("unknown log target type: %q", target[0])
	}

	r.Use(gin.LoggerWithConfig(logConfig))

	return nil
}

Christian Pointner's avatar
Christian Pointner committed
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
type HealthStatus struct {
	err error
}

func (s HealthStatus) String() string {
	if s.err == nil {
		return "OK"
	}
	return s.err.Error()
}

func (s HealthStatus) MarshalText() (data []byte, err error) {
	data = []byte(s.String())
	return
}

type Health struct {
	Auth     HealthStatus `json:"auth"`
	Store    HealthStatus `json:"store"`
	Importer HealthStatus `json:"importer"`
}

117
118
119
120
121
122
123
// healthzHandler checks daemon health.
// @Summary      Check health
// @Description  Checks daemon health.
// @Produce      json
// @Success      200  {object}  Health
// @Failure      503  {object}  Health
// @Router       /healthz [get]
Christian Pointner's avatar
Christian Pointner committed
124
func healthzHandler(c *gin.Context, st *store.Store, im *importer.Importer) {
125
126
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // TODO: hardcoded value
	defer cancel()
Christian Pointner's avatar
Christian Pointner committed
127

128
129
130
131
132
133
134
135
	var h Health
	var wg sync.WaitGroup
	wg.Add(3)
	go func() { defer wg.Done(); h.Auth.err = auth.Healthz(ctx) }()
	go func() { defer wg.Done(); h.Store.err = st.Healthz(ctx) }()
	go func() { defer wg.Done(); h.Importer.err = im.Healthz(ctx) }()

	wg.Wait()
Christian Pointner's avatar
Christian Pointner committed
136
137
138
139
140
141
142
	code := http.StatusOK
	if h.Auth.err != nil || h.Store.err != nil || h.Importer.err != nil {
		code = http.StatusServiceUnavailable
	}
	c.JSON(code, h)
}

143
func runWeb(ln net.Listener, st *store.Store, im *importer.Importer, conf WebConfig) error {
144
	gin.SetMode(gin.ReleaseMode)
145

Christian Pointner's avatar
Christian Pointner committed
146
147
	r := gin.New()
	r.Use(gin.Recovery())
148
149
150
	if err := installLogger(r, conf); err != nil {
		return err
	}
151
	r.HandleMethodNotAllowed = true
152
153
154
155
156
157
158
159
160
161
162
163
164
	if conf.Cors != nil {
		c := cors.New(cors.Options{
			AllowedOrigins:     conf.Cors.AllowedOrigins,
			AllowedMethods:     conf.Cors.AllowedMethods,
			AllowedHeaders:     conf.Cors.AllowedHeaders,
			ExposedHeaders:     conf.Cors.ExposedHeaders,
			MaxAge:             conf.Cors.MaxAge,
			AllowCredentials:   conf.Cors.AllowCredentials,
			OptionsPassthrough: conf.Cors.OptionsPassthrough,
			Debug:              conf.Cors.Debug,
		})
		r.Use(c)
	}
165

166
167
168
169
170
	if conf.DebugUI.Enabled {
		infoLog.Printf("web: enabling debug ui")
		r.GET("/", func(c *gin.Context) { c.Redirect(http.StatusSeeOther, WebUIPathPrefix) })
		r.StaticFS(WebUIPathPrefix, ui.Assets)
	}
Christian Pointner's avatar
Christian Pointner committed
171
172

	apiV1.InstallHTTPHandler(r.Group(WebAPIv1Prefix), st, im, infoLog, errLog, dbgLog)
173
	auth.InstallHTTPHandler(r.Group(WebAuthPrefix))
Christian Pointner's avatar
Christian Pointner committed
174
	r.GET(HealthzEndpoint, func(c *gin.Context) { healthzHandler(c, st, im) })
175
	r.GET(WebAPIDocsPrefix+"*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
176

177
178
	srv := &http.Server{
		Handler:      r,
179
180
		WriteTimeout: 12 * time.Hour,
		ReadTimeout:  12 * time.Hour,
181
	}
182
183
184
185
186
187
	if conf.ReadTimeout > 0 {
		srv.ReadTimeout = conf.ReadTimeout
	}
	if conf.WriteTimeout > 0 {
		srv.WriteTimeout = conf.WriteTimeout
	}
188
189

	infoLog.Printf("web: listening on %s", ln.Addr())
190
191
	return srv.Serve(ln)
}