2023-06-19 14:42:47 -07:00
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
2018-11-11 15:32:48 -08:00
2023-08-10 19:46:45 -07:00
package httpd // import "miniflux.app/v2/internal/http/server"
2018-11-11 15:32:48 -08:00
import (
2025-06-12 23:25:15 +02:00
"crypto/tls"
2023-09-24 16:32:09 -07:00
"fmt"
"log/slog"
2018-11-11 16:21:57 -08:00
"net"
2018-11-11 15:32:48 -08:00
"net/http"
2018-11-11 16:21:57 -08:00
"os"
2018-11-25 17:41:16 -08:00
"strconv"
2018-11-11 16:21:57 -08:00
"strings"
2018-11-11 15:32:48 -08:00
"time"
2023-08-10 19:46:45 -07:00
"miniflux.app/v2/internal/api"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/fever"
"miniflux.app/v2/internal/googlereader"
"miniflux.app/v2/internal/http/request"
"miniflux.app/v2/internal/storage"
"miniflux.app/v2/internal/ui"
"miniflux.app/v2/internal/version"
"miniflux.app/v2/internal/worker"
2018-11-11 15:32:48 -08:00
"github.com/gorilla/mux"
2020-09-27 16:01:06 -07:00
"github.com/prometheus/client_golang/prometheus/promhttp"
2022-07-01 19:32:28 -07:00
"golang.org/x/crypto/acme"
2018-11-11 15:32:48 -08:00
"golang.org/x/crypto/acme/autocert"
)
2025-06-12 23:25:15 +02:00
func StartWebServer ( store * storage . Storage , pool * worker . Pool ) [ ] * http . Server {
listenAddresses := config . Opts . ListenAddr ( )
var httpServers [ ] * http . Server
2019-06-01 18:18:09 -07:00
certFile := config . Opts . CertFile ( )
keyFile := config . Opts . CertKeyFile ( )
certDomain := config . Opts . CertDomain ( )
2025-06-12 23:25:15 +02:00
var sharedAutocertTLSConfig * tls . Config
if certDomain != "" {
slog . Debug ( "Configuring autocert manager and shared TLS config" , slog . String ( "domain" , certDomain ) )
certManager := autocert . Manager {
Cache : storage . NewCertificateCache ( store ) ,
Prompt : autocert . AcceptTOS ,
HostPolicy : autocert . HostWhitelist ( certDomain ) ,
}
2018-11-11 15:32:48 -08:00
2025-06-12 23:25:15 +02:00
sharedAutocertTLSConfig = & tls . Config { }
sharedAutocertTLSConfig . GetCertificate = certManager . GetCertificate
sharedAutocertTLSConfig . NextProtos = [ ] string { "h2" , "http/1.1" , acme . ALPNProto }
challengeServer := & http . Server {
Handler : certManager . HTTPHandler ( nil ) ,
Addr : ":http" ,
}
slog . Info ( "Starting ACME HTTP challenge server for autocert" , slog . String ( "address" , challengeServer . Addr ) )
go func ( ) {
if err := challengeServer . ListenAndServe ( ) ; err != http . ErrServerClosed {
slog . Error ( "ACME HTTP challenge server failed" , slog . Any ( "error" , err ) )
}
} ( )
2019-06-01 18:18:09 -07:00
config . Opts . HTTPS = true
2025-06-12 23:25:15 +02:00
httpServers = append ( httpServers , challengeServer )
2018-11-11 15:32:48 -08:00
}
2025-06-12 23:25:15 +02:00
for i , listenAddr := range listenAddresses {
server := & http . Server {
ReadTimeout : time . Duration ( config . Opts . HTTPServerTimeout ( ) ) * time . Second ,
WriteTimeout : time . Duration ( config . Opts . HTTPServerTimeout ( ) ) * time . Second ,
IdleTimeout : time . Duration ( config . Opts . HTTPServerTimeout ( ) ) * time . Second ,
Handler : setupHandler ( store , pool ) ,
}
if ! strings . HasPrefix ( listenAddr , "/" ) && os . Getenv ( "LISTEN_PID" ) != strconv . Itoa ( os . Getpid ( ) ) {
server . Addr = listenAddr
}
shouldAddServer := true
switch {
case os . Getenv ( "LISTEN_PID" ) == strconv . Itoa ( os . Getpid ( ) ) :
if i == 0 {
slog . Info ( "Starting server using systemd socket for the first listen address" , slog . String ( "address_info" , listenAddr ) )
startSystemdSocketServer ( server )
} else {
slog . Warn ( "Systemd socket activation: Only the first listen address is used by systemd. Other addresses ignored." , slog . String ( "skipped_address" , listenAddr ) )
shouldAddServer = false
}
case strings . HasPrefix ( listenAddr , "/" ) : // Unix socket
startUnixSocketServer ( server , listenAddr )
case certDomain != "" && ( listenAddr == ":https" || ( i == 0 && strings . Contains ( listenAddr , ":" ) ) ) :
server . Addr = listenAddr
startAutoCertTLSServer ( server , sharedAutocertTLSConfig )
case certFile != "" && keyFile != "" :
server . Addr = listenAddr
startTLSServer ( server , certFile , keyFile )
config . Opts . HTTPS = true
default :
server . Addr = listenAddr
startHTTPServer ( server )
}
if shouldAddServer {
httpServers = append ( httpServers , server )
}
}
return httpServers
2018-11-11 15:32:48 -08:00
}
2018-11-25 17:41:16 -08:00
func startSystemdSocketServer ( server * http . Server ) {
go func ( ) {
f := os . NewFile ( 3 , "systemd socket" )
listener , err := net . FileListener ( f )
if err != nil {
2023-09-24 16:32:09 -07:00
printErrorAndExit ( ` Unable to create listener from systemd socket: %v ` , err )
2018-11-25 17:41:16 -08:00
}
2023-09-24 16:32:09 -07:00
slog . Info ( ` Starting server using systemd socket ` )
2018-11-25 17:41:16 -08:00
if err := server . Serve ( listener ) ; err != http . ErrServerClosed {
2025-06-12 23:25:15 +02:00
printErrorAndExit ( ` Systemd socket server failed to start: %v ` , err )
2018-11-25 17:41:16 -08:00
}
} ( )
}
2018-11-11 16:21:57 -08:00
func startUnixSocketServer ( server * http . Server , socketFile string ) {
2025-06-12 23:25:15 +02:00
if err := os . Remove ( socketFile ) ; err != nil && ! os . IsNotExist ( err ) {
printErrorAndExit ( "Unable to remove existing Unix socket %s: %v" , socketFile , err )
}
listener , err := net . Listen ( "unix" , socketFile )
if err != nil {
printErrorAndExit ( ` Server failed to listen on Unix socket %s: %v ` , socketFile , err )
}
2018-11-11 16:21:57 -08:00
2025-06-12 23:25:15 +02:00
if err := os . Chmod ( socketFile , 0666 ) ; err != nil {
printErrorAndExit ( ` Unable to change socket permission for %s: %v ` , socketFile , err )
}
2018-11-25 16:13:52 -08:00
2025-06-12 23:25:15 +02:00
go func ( ) {
slog . Info ( "Starting server using a Unix socket" , slog . String ( "socket" , socketFile ) )
2018-11-11 16:21:57 -08:00
if err := server . Serve ( listener ) ; err != http . ErrServerClosed {
2025-06-12 23:25:15 +02:00
printErrorAndExit ( fmt . Sprintf ( "Unix socket server failed to start on %s: %%v" , socketFile ) , err )
2018-11-11 16:21:57 -08:00
}
2025-06-12 23:25:15 +02:00
} ( )
2018-11-11 16:21:57 -08:00
}
2025-06-12 23:25:15 +02:00
func startAutoCertTLSServer ( server * http . Server , autoTLSConfig * tls . Config ) {
server . TLSConfig . GetCertificate = autoTLSConfig . GetCertificate
server . TLSConfig . NextProtos = autoTLSConfig . NextProtos
2018-11-11 15:32:48 -08:00
go func ( ) {
2023-09-24 16:32:09 -07:00
slog . Info ( "Starting TLS server using automatic certificate management" ,
slog . String ( "listen_address" , server . Addr ) ,
)
2020-03-03 00:30:48 -05:00
if err := server . ListenAndServeTLS ( "" , "" ) ; err != http . ErrServerClosed {
2025-06-12 23:25:15 +02:00
printErrorAndExit ( fmt . Sprintf ( "Autocert server failed to start on %s: %%v" , server . Addr ) , err )
2018-11-11 15:32:48 -08:00
}
} ( )
}
func startTLSServer ( server * http . Server , certFile , keyFile string ) {
go func ( ) {
2023-09-24 16:32:09 -07:00
slog . Info ( "Starting TLS server using a certificate" ,
slog . String ( "listen_address" , server . Addr ) ,
slog . String ( "cert_file" , certFile ) ,
slog . String ( "key_file" , keyFile ) ,
)
2018-11-11 15:32:48 -08:00
if err := server . ListenAndServeTLS ( certFile , keyFile ) ; err != http . ErrServerClosed {
2025-06-12 23:25:15 +02:00
printErrorAndExit ( fmt . Sprintf ( "TLS server failed to start on %s: %%v" , server . Addr ) , err )
2018-11-11 15:32:48 -08:00
}
} ( )
}
func startHTTPServer ( server * http . Server ) {
go func ( ) {
2023-09-24 16:32:09 -07:00
slog . Info ( "Starting HTTP server" ,
slog . String ( "listen_address" , server . Addr ) ,
)
2018-11-11 15:32:48 -08:00
if err := server . ListenAndServe ( ) ; err != http . ErrServerClosed {
2025-06-12 23:25:15 +02:00
printErrorAndExit ( fmt . Sprintf ( "HTTP server failed to start on %s: %%v" , server . Addr ) , err )
2018-11-11 15:32:48 -08:00
}
} ( )
}
2021-01-02 16:33:41 -08:00
func setupHandler ( store * storage . Storage , pool * worker . Pool ) * mux . Router {
2025-05-24 20:28:39 -07:00
livenessProbe := func ( w http . ResponseWriter , r * http . Request ) {
w . WriteHeader ( http . StatusOK )
w . Write ( [ ] byte ( "OK" ) )
}
readinessProbe := func ( w http . ResponseWriter , r * http . Request ) {
if err := store . Ping ( ) ; err != nil {
http . Error ( w , fmt . Sprintf ( "Database Connection Error: %q" , err ) , http . StatusServiceUnavailable )
return
}
w . WriteHeader ( http . StatusOK )
w . Write ( [ ] byte ( "OK" ) )
}
2018-11-11 15:32:48 -08:00
router := mux . NewRouter ( )
2025-05-24 20:28:39 -07:00
// These routes do not take the base path into consideration and are always available at the root of the server.
router . HandleFunc ( "/liveness" , livenessProbe ) . Name ( "liveness" )
router . HandleFunc ( "/healthz" , livenessProbe ) . Name ( "healthz" )
router . HandleFunc ( "/readiness" , readinessProbe ) . Name ( "readiness" )
router . HandleFunc ( "/readyz" , readinessProbe ) . Name ( "readyz" )
var subrouter * mux . Router
2019-06-01 18:18:09 -07:00
if config . Opts . BasePath ( ) != "" {
2025-05-24 20:28:39 -07:00
subrouter = router . PathPrefix ( config . Opts . BasePath ( ) ) . Subrouter ( )
} else {
subrouter = router . NewRoute ( ) . Subrouter ( )
2018-11-11 15:32:48 -08:00
}
2020-09-12 18:31:45 -07:00
if config . Opts . HasMaintenanceMode ( ) {
2025-05-24 20:28:39 -07:00
subrouter . Use ( func ( next http . Handler ) http . Handler {
2020-09-12 18:31:45 -07:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Write ( [ ] byte ( config . Opts . MaintenanceMessage ( ) ) )
} )
} )
}
2025-05-24 20:28:39 -07:00
subrouter . Use ( middleware )
2018-11-11 15:32:48 -08:00
2025-05-24 20:28:39 -07:00
fever . Serve ( subrouter , store )
googlereader . Serve ( subrouter , store )
api . Serve ( subrouter , store , pool )
ui . Serve ( subrouter , store , pool )
2018-11-11 15:32:48 -08:00
2025-05-24 20:28:39 -07:00
subrouter . HandleFunc ( "/healthcheck" , readinessProbe ) . Name ( "healthcheck" )
2020-09-27 16:01:06 -07:00
2025-05-24 20:28:39 -07:00
subrouter . HandleFunc ( "/version" , func ( w http . ResponseWriter , r * http . Request ) {
2020-04-11 22:08:17 +02:00
w . Write ( [ ] byte ( version . Version ) )
} ) . Name ( "version" )
2018-12-28 13:41:26 -08:00
2020-09-27 16:01:06 -07:00
if config . Opts . HasMetricsCollector ( ) {
2025-05-24 20:28:39 -07:00
subrouter . Handle ( "/metrics" , promhttp . Handler ( ) ) . Name ( "metrics" )
subrouter . Use ( func ( next http . Handler ) http . Handler {
2020-09-27 16:01:06 -07:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
route := mux . CurrentRoute ( r )
// Returns a 404 if the client is not authorized to access the metrics endpoint.
if route . GetName ( ) == "metrics" && ! isAllowedToAccessMetricsEndpoint ( r ) {
2023-09-24 16:32:09 -07:00
slog . Warn ( "Authentication failed while accessing the metrics endpoint" ,
slog . String ( "client_ip" , request . ClientIP ( r ) ) ,
slog . String ( "client_user_agent" , r . UserAgent ( ) ) ,
slog . String ( "client_remote_addr" , r . RemoteAddr ) ,
)
2020-09-27 16:01:06 -07:00
http . NotFound ( w , r )
return
}
next . ServeHTTP ( w , r )
} )
} )
}
2018-11-11 15:32:48 -08:00
return router
}
2020-09-27 16:01:06 -07:00
func isAllowedToAccessMetricsEndpoint ( r * http . Request ) bool {
2023-09-24 16:32:09 -07:00
clientIP := request . ClientIP ( r )
2023-03-11 20:04:27 -08:00
if config . Opts . MetricsUsername ( ) != "" && config . Opts . MetricsPassword ( ) != "" {
username , password , authOK := r . BasicAuth ( )
if ! authOK {
2023-09-24 16:32:09 -07:00
slog . Warn ( "Metrics endpoint accessed without authentication header" ,
slog . Bool ( "authentication_failed" , true ) ,
slog . String ( "client_ip" , clientIP ) ,
slog . String ( "client_user_agent" , r . UserAgent ( ) ) ,
slog . String ( "client_remote_addr" , r . RemoteAddr ) ,
)
2023-03-11 20:04:27 -08:00
return false
}
if username == "" || password == "" {
2023-09-24 16:32:09 -07:00
slog . Warn ( "Metrics endpoint accessed with empty username or password" ,
slog . Bool ( "authentication_failed" , true ) ,
slog . String ( "client_ip" , clientIP ) ,
slog . String ( "client_user_agent" , r . UserAgent ( ) ) ,
slog . String ( "client_remote_addr" , r . RemoteAddr ) ,
)
2023-03-11 20:04:27 -08:00
return false
}
if username != config . Opts . MetricsUsername ( ) || password != config . Opts . MetricsPassword ( ) {
2023-09-24 16:32:09 -07:00
slog . Warn ( "Metrics endpoint accessed with invalid username or password" ,
slog . Bool ( "authentication_failed" , true ) ,
slog . String ( "client_ip" , clientIP ) ,
slog . String ( "client_user_agent" , r . UserAgent ( ) ) ,
slog . String ( "client_remote_addr" , r . RemoteAddr ) ,
)
2023-03-11 20:04:27 -08:00
return false
}
}
2020-09-27 16:01:06 -07:00
2023-12-06 19:48:05 +01:00
remoteIP := request . FindRemoteIP ( r )
if remoteIP == "@" {
// This indicates a request sent via a Unix socket, always consider these trusted.
return true
}
2020-09-27 16:01:06 -07:00
for _ , cidr := range config . Opts . MetricsAllowedNetworks ( ) {
_ , network , err := net . ParseCIDR ( cidr )
if err != nil {
2023-09-24 16:32:09 -07:00
slog . Error ( "Metrics endpoint accessed with invalid CIDR" ,
slog . Bool ( "authentication_failed" , true ) ,
slog . String ( "client_ip" , clientIP ) ,
slog . String ( "client_user_agent" , r . UserAgent ( ) ) ,
slog . String ( "client_remote_addr" , r . RemoteAddr ) ,
slog . String ( "cidr" , cidr ) ,
)
return false
2020-09-27 16:01:06 -07:00
}
2023-03-11 20:36:52 -08:00
// We use r.RemoteAddr in this case because HTTP headers like X-Forwarded-For can be easily spoofed.
// The recommendation is to use HTTP Basic authentication.
2023-12-06 19:48:05 +01:00
if network . Contains ( net . ParseIP ( remoteIP ) ) {
2020-09-27 16:01:06 -07:00
return true
}
}
return false
}
2023-09-24 16:32:09 -07:00
func printErrorAndExit ( format string , a ... any ) {
message := fmt . Sprintf ( format , a ... )
slog . Error ( message )
fmt . Fprintf ( os . Stderr , "%v\n" , message )
os . Exit ( 1 )
}