2016-10-25 15:20:06 +00:00
package server
import (
2017-02-13 03:33:27 +00:00
"crypto/tls"
2016-10-24 17:08:36 +00:00
"math/rand"
2016-10-25 15:20:06 +00:00
"net"
"net/http"
2016-10-24 17:08:36 +00:00
"runtime"
2016-10-25 15:20:06 +00:00
"strconv"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt"
"github.com/influxdata/chronograf/canned"
"github.com/influxdata/chronograf/layouts"
clog "github.com/influxdata/chronograf/log"
2017-02-23 22:17:28 +00:00
"github.com/influxdata/chronograf/oauth2"
2016-10-25 15:20:06 +00:00
"github.com/influxdata/chronograf/uuid"
2016-10-24 17:08:36 +00:00
client "github.com/influxdata/usage-client/v1"
2017-02-13 03:33:27 +00:00
flags "github.com/jessevdk/go-flags"
2016-10-25 15:20:06 +00:00
"github.com/tylerb/graceful"
)
2017-01-27 21:47:46 +00:00
var (
startTime time . Time
basepath string
)
2016-12-19 20:09:59 +00:00
func init ( ) {
startTime = time . Now ( ) . UTC ( )
}
2016-10-25 15:20:06 +00:00
// Server for the chronograf API
type Server struct {
2017-02-13 03:33:27 +00:00
Host string ` long:"host" description:"The IP to listen on" default:"0.0.0.0" env:"HOST" `
Port int ` long:"port" description:"The port to listen on for insecure connections, defaults to a random value" default:"8888" env:"PORT" `
2016-10-25 15:20:06 +00:00
2017-02-13 03:33:27 +00:00
Cert flags . Filename ` long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE" `
Key flags . Filename ` long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY" `
2016-10-25 15:20:06 +00:00
2017-02-16 17:56:59 +00:00
Develop bool ` short:"d" long:"develop" description:"Run server in develop mode." `
BoltPath string ` short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db" `
CannedPath string ` short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned" `
TokenSecret string ` short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET" `
2017-01-05 17:29:26 +00:00
GithubClientID string ` short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID" `
GithubClientSecret string ` short:"s" long:"github-client-secret" description:"Github Client Secret for OAuth 2 support" env:"GH_CLIENT_SECRET" `
GithubOrgs [ ] string ` short:"o" long:"github-organization" description:"Github organization user is required to have active membership" env:"GH_ORGS" env-delim:"," `
2017-02-16 17:56:59 +00:00
2017-02-15 05:11:11 +00:00
GoogleClientID string ` long:"google-client-id" description:"Google Client ID for OAuth 2 support" env:"GOOGLE_CLIENT_ID" `
GoogleClientSecret string ` long:"google-client-secret" description:"Google Client Secret for OAuth 2 support" env:"GOGGLE_CLIENT_SECRET" `
GoogleDomains [ ] string ` long:"google-domains" description:"Google email domain user is required to have active membership" env:"GOOGLE_DOMAINS" env-delim:"," `
2017-02-15 05:34:15 +00:00
PublicURL string ` long:"public-url" description:"Full public URL used to access Chronograf from a web browser. Used for Google OAuth2 authentication. (http://localhost:8888)" env:"PUBLIC_URL" `
2017-02-15 05:11:11 +00:00
2017-02-21 18:04:17 +00:00
HerokuClientID string ` long:"heroku-client-id" description:"Heroku Client ID for OAuth 2 support" env:"HEROKU_CLIENT_ID" `
HerokuSecret string ` long:"heroku-secret" description:"Heroku Secret for OAuth 2 support" env:"HEROKU_SECRET" `
HerokuOrganizations [ ] string ` long:"heroku-organization" description:"Heroku Organization Memberships a user is required to have for access to Chronograf (comma separated)" env:"HEROKU_ORGS" env-delim:"," `
2017-02-16 17:56:59 +00:00
2017-02-15 05:11:11 +00:00
ReportingDisabled bool ` short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED" `
LogLevel string ` short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL" `
Basepath string ` short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH" `
ShowVersion bool ` short:"v" long:"version" description:"Show Chronograf version info" `
BuildInfo BuildInfo
Listener net . Listener
handler http . Handler
2016-10-25 15:20:06 +00:00
}
2017-02-23 22:17:28 +00:00
func provide ( p oauth2 . Provider , m oauth2 . Mux , ok func ( ) bool ) func ( func ( oauth2 . Provider , oauth2 . Mux ) ) {
return func ( configure func ( oauth2 . Provider , oauth2 . Mux ) ) {
if ok ( ) {
configure ( p , m )
}
}
}
func ( s * Server ) UseGithub ( ) bool {
return s . TokenSecret != "" && s . GithubClientID != "" && s . GithubClientSecret != ""
}
func ( s * Server ) UseGoogle ( ) bool {
return s . TokenSecret != "" && s . GoogleClientID != "" && s . GoogleClientSecret != "" && s . PublicURL != ""
}
func ( s * Server ) UseHeroku ( ) bool {
return s . TokenSecret != "" && s . HerokuClientID != "" && s . HerokuSecret != ""
}
func ( s * Server ) githubOAuth ( logger chronograf . Logger , auth oauth2 . Authenticator ) ( oauth2 . Provider , oauth2 . Mux , func ( ) bool ) {
gh := oauth2 . Github {
ClientID : s . GithubClientID ,
ClientSecret : s . GithubClientSecret ,
Orgs : s . GithubOrgs ,
Logger : logger ,
}
ghMux := oauth2 . NewCookieMux ( & gh , auth , logger )
return & gh , ghMux , s . UseGithub
}
func ( s * Server ) googleOAuth ( logger chronograf . Logger , auth oauth2 . Authenticator ) ( oauth2 . Provider , oauth2 . Mux , func ( ) bool ) {
redirectURL := s . PublicURL + s . Basepath + "/oauth/google/callback"
google := oauth2 . Google {
ClientID : s . GoogleClientID ,
ClientSecret : s . GoogleClientSecret ,
Domains : s . GoogleDomains ,
RedirectURL : redirectURL ,
Logger : logger ,
}
goMux := oauth2 . NewCookieMux ( & google , auth , logger )
return & google , goMux , s . UseGoogle
}
func ( s * Server ) herokuOAuth ( logger chronograf . Logger , auth oauth2 . Authenticator ) ( oauth2 . Provider , oauth2 . Mux , func ( ) bool ) {
heroku := oauth2 . Heroku {
ClientID : s . HerokuClientID ,
ClientSecret : s . HerokuSecret ,
Organizations : s . HerokuOrganizations ,
Logger : logger ,
}
hMux := oauth2 . NewCookieMux ( & heroku , auth , logger )
return & heroku , hMux , s . UseHeroku
2016-10-25 15:20:06 +00:00
}
2016-11-19 17:41:06 +00:00
// BuildInfo is sent to the usage client to track versions and commits
2016-10-24 17:08:36 +00:00
type BuildInfo struct {
Version string
Commit string
}
2016-10-28 16:27:06 +00:00
func ( s * Server ) useAuth ( ) bool {
2017-02-15 05:11:11 +00:00
gh := s . TokenSecret != "" && s . GithubClientID != "" && s . GithubClientSecret != ""
2017-02-15 05:34:15 +00:00
google := s . TokenSecret != "" && s . GoogleClientID != "" && s . GoogleClientSecret != "" && s . PublicURL != ""
2017-02-16 17:56:59 +00:00
heroku := s . TokenSecret != "" && s . HerokuClientID != "" && s . HerokuSecret != ""
return gh || google || heroku
2016-10-28 16:27:06 +00:00
}
2016-10-25 15:20:06 +00:00
2017-02-13 03:33:27 +00:00
func ( s * Server ) useTLS ( ) bool {
return s . Cert != ""
}
2017-02-13 03:48:12 +00:00
// NewListener will an http or https listener depending useTLS()
2017-02-13 03:33:27 +00:00
func ( s * Server ) NewListener ( ) ( net . Listener , error ) {
addr := net . JoinHostPort ( s . Host , strconv . Itoa ( s . Port ) )
if ! s . useTLS ( ) {
listener , err := net . Listen ( "tcp" , addr )
if err != nil {
return nil , err
}
return listener , nil
}
// If no key specified, therefore, we assume it is in the cert
if s . Key == "" {
s . Key = s . Cert
}
cert , err := tls . LoadX509KeyPair ( string ( s . Cert ) , string ( s . Key ) )
if err != nil {
return nil , err
}
listener , err := tls . Listen ( "tcp" , addr , & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
} )
if err != nil {
return nil , err
}
return listener , nil
}
2016-10-28 16:27:06 +00:00
// Serve starts and runs the chronograf server
func ( s * Server ) Serve ( ) error {
2016-10-24 17:08:36 +00:00
logger := clog . New ( clog . ParseLevel ( s . LogLevel ) )
2016-11-18 00:52:19 +00:00
service := openService ( s . BoltPath , s . CannedPath , logger , s . useAuth ( ) )
2017-01-27 21:47:46 +00:00
basepath = s . Basepath
2017-02-13 03:33:27 +00:00
2017-02-23 22:17:28 +00:00
providerFuncs := [ ] func ( func ( oauth2 . Provider , oauth2 . Mux ) ) { }
auth := oauth2 . NewJWT ( s . TokenSecret )
providerFuncs = append ( providerFuncs , provide ( s . githubOAuth ( logger , & auth ) ) )
providerFuncs = append ( providerFuncs , provide ( s . googleOAuth ( logger , & auth ) ) )
providerFuncs = append ( providerFuncs , provide ( s . herokuOAuth ( logger , & auth ) ) )
2016-10-25 15:20:06 +00:00
s . handler = NewMux ( MuxOpts {
2017-02-23 22:17:28 +00:00
Develop : s . Develop ,
TokenSecret : s . TokenSecret ,
Logger : logger ,
UseAuth : s . useAuth ( ) ,
ProviderFuncs : providerFuncs ,
2016-10-28 16:27:06 +00:00
} , service )
2016-10-25 15:20:06 +00:00
2017-02-13 03:33:27 +00:00
// Add chronograf's version header to all requests
2017-01-13 23:10:50 +00:00
s . handler = Version ( s . BuildInfo . Version , s . handler )
2017-02-13 03:33:27 +00:00
if s . useTLS ( ) {
// Add HSTS to instruct all browsers to change from http to https
s . handler = HSTS ( s . handler )
}
listener , err := s . NewListener ( )
2016-10-25 15:20:06 +00:00
if err != nil {
2016-10-28 16:27:06 +00:00
logger .
WithField ( "component" , "server" ) .
Error ( err )
2016-10-25 15:20:06 +00:00
return err
}
2017-02-13 03:33:27 +00:00
s . Listener = listener
2016-10-25 15:20:06 +00:00
httpServer := & graceful . Server { Server : new ( http . Server ) }
httpServer . SetKeepAlivesEnabled ( true )
2016-12-07 23:17:27 +00:00
httpServer . TCPKeepAlive = 5 * time . Second
2016-10-25 15:20:06 +00:00
httpServer . Handler = s . handler
2016-10-24 17:08:36 +00:00
if ! s . ReportingDisabled {
go reportUsageStats ( s . BuildInfo , logger )
}
2017-02-13 03:33:27 +00:00
scheme := "http"
if s . useTLS ( ) {
scheme = "https"
}
2016-10-28 16:27:06 +00:00
logger .
WithField ( "component" , "server" ) .
2017-02-13 03:33:27 +00:00
Info ( "Serving chronograf at " , scheme , "://" , s . Listener . Addr ( ) )
2016-10-28 16:27:06 +00:00
if err := httpServer . Serve ( s . Listener ) ; err != nil {
logger .
WithField ( "component" , "server" ) .
Error ( err )
return err
}
logger .
WithField ( "component" , "server" ) .
2017-02-13 03:33:27 +00:00
Info ( "Stopped serving chronograf at " , scheme , "://" , s . Listener . Addr ( ) )
2016-10-28 16:27:06 +00:00
2016-10-25 15:20:06 +00:00
return nil
}
2016-10-28 16:27:06 +00:00
2016-11-18 00:52:19 +00:00
func openService ( boltPath , cannedPath string , logger chronograf . Logger , useAuth bool ) Service {
2016-10-28 16:27:06 +00:00
db := bolt . NewClient ( )
db . Path = boltPath
if err := db . Open ( ) ; err != nil {
logger .
WithField ( "component" , "boltstore" ) .
2016-11-08 02:50:06 +00:00
Fatal ( "Unable to open boltdb; is there a chronograf already running? " , err )
2016-10-28 16:27:06 +00:00
}
2016-11-15 01:07:38 +00:00
// These apps are those handled from a directory
2016-11-07 16:10:26 +00:00
apps := canned . NewApps ( cannedPath , & uuid . V4 { } , logger )
2016-11-15 01:07:38 +00:00
// These apps are statically compiled into chronograf
2016-11-15 04:25:38 +00:00
binApps := & canned . BinLayoutStore {
2016-11-15 01:07:38 +00:00
Logger : logger ,
}
// Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts.
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
// the operation has success. So, the database is preferred over filesystem over binary data.
2016-10-28 16:27:06 +00:00
layouts := & layouts . MultiLayoutStore {
Stores : [ ] chronograf . LayoutStore {
db . LayoutStore ,
apps ,
2016-11-15 04:25:38 +00:00
binApps ,
2016-10-28 16:27:06 +00:00
} ,
}
return Service {
2017-02-24 03:54:20 +00:00
TimeSeriesClient : & InfluxClient { } ,
SourcesStore : db . SourcesStore ,
ServersStore : db . ServersStore ,
UsersStore : db . UsersStore ,
LayoutStore : layouts ,
DashboardsStore : db . DashboardsStore ,
AlertRulesStore : db . AlertsStore ,
Logger : logger ,
UseAuth : useAuth ,
2016-10-28 16:27:06 +00:00
}
}
2016-10-24 17:08:36 +00:00
// reportUsageStats starts periodic server reporting.
func reportUsageStats ( bi BuildInfo , logger chronograf . Logger ) {
rand . Seed ( time . Now ( ) . UTC ( ) . UnixNano ( ) )
serverID := strconv . FormatUint ( uint64 ( rand . Int63 ( ) ) , 10 )
reporter := client . New ( "" )
u := & client . Usage {
2016-11-16 23:25:12 +00:00
Product : "chronograf-ng" ,
2016-10-24 17:08:36 +00:00
Data : [ ] client . UsageData {
{
Values : client . Values {
"os" : runtime . GOOS ,
"arch" : runtime . GOARCH ,
"version" : bi . Version ,
"cluster_id" : serverID ,
2016-12-19 21:29:33 +00:00
"uptime" : time . Since ( startTime ) . Seconds ( ) ,
2016-10-24 17:08:36 +00:00
} ,
} ,
} ,
}
l := logger . WithField ( "component" , "usage" ) .
WithField ( "reporting_addr" , reporter . URL ) .
WithField ( "freq" , "24h" ) .
2016-12-19 21:29:33 +00:00
WithField ( "stats" , "os,arch,version,cluster_id,uptime" )
2016-10-24 17:08:36 +00:00
l . Info ( "Reporting usage stats" )
2016-12-20 20:59:56 +00:00
_ , _ = reporter . Save ( u )
2016-10-24 17:08:36 +00:00
ticker := time . NewTicker ( 24 * time . Hour )
defer ticker . Stop ( )
for {
2016-12-20 20:59:56 +00:00
<- ticker . C
l . Debug ( "Reporting usage stats" )
go reporter . Save ( u )
2016-10-24 17:08:36 +00:00
}
}