From a88a1e75edaf0f1b7827d83ad5601553832ef0a3 Mon Sep 17 00:00:00 2001 From: Shane Neuerburg Date: Thu, 10 Nov 2016 10:57:55 -0700 Subject: [PATCH] Add basic authentication to HTTP API This adds support for basic authentication for the HTTP API with new configuration values to control if basic auth is used, the basic auth realm URL, and the location to a basic .htpasswd file. github.com/abbot/go-http-auth is added as a dependency as it efficiently implements basic auth, as well as .htpasswd file monitoring. --- cmd/cgr-engine/cgr-engine.go | 7 ++++++- config/config.go | 20 ++++++++++++++++++++ config/config_defaults.go | 11 +++++++++-- config/config_json.go | 13 +++++++++++++ config/libconfig_json.go | 7 +++++++ data/conf/cgrates/cgrates.json | 8 +++++++- glide.lock | 19 +++++++++++++++---- glide.yaml | 2 ++ utils/server.go | 28 +++++++++++++++++----------- 9 files changed, 96 insertions(+), 19 deletions(-) diff --git a/cmd/cgr-engine/cgr-engine.go b/cmd/cgr-engine/cgr-engine.go index 7f8df125b..32253a4b3 100644 --- a/cmd/cgr-engine/cgr-engine.go +++ b/cmd/cgr-engine/cgr-engine.go @@ -530,7 +530,12 @@ func startRpc(server *utils.Server, internalRaterChan, } go server.ServeJSON(cfg.RPCJSONListen) go server.ServeGOB(cfg.RPCGOBListen) - go server.ServeHTTP(cfg.HTTPListen) + go server.ServeHTTP( + cfg.HTTPListen, + cfg.HTTPApiUseBasicAuth, + cfg.HTTPApiBasicAuthRealm, + cfg.HTTPApiHtpasswdFile, + ) } func writePid() { diff --git a/config/config.go b/config/config.go index a7acb3ed2..3b255e9c4 100644 --- a/config/config.go +++ b/config/config.go @@ -203,6 +203,9 @@ type CGRConfig struct { RPCJSONListen string // RPC JSON listening address RPCGOBListen string // RPC GOB listening address HTTPListen string // HTTP listening address + HTTPApiUseBasicAuth bool // Use basic auth for HTTP API + HTTPApiBasicAuthRealm string // Basic auth realm URL + HTTPApiHtpasswdFile string // Basic auth htpasswd file path DefaultReqType string // Use this request type if not defined on top DefaultCategory string // set default type of record DefaultTenant string // set default tenant @@ -503,6 +506,11 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { return err } + jsnHttpApiCfg, err := jsnCfg.HttpApiJsonCfg() + if err != nil { + return err + } + jsnTpDbCfg, err := jsnCfg.DbJsonCfg(TPDB_JSN) if err != nil { return err @@ -779,6 +787,18 @@ func (self *CGRConfig) loadFromJsonCfg(jsnCfg *CgrJsonCfg) error { } } + if jsnHttpApiCfg != nil { + if jsnHttpApiCfg.Use_basic_auth != nil { + self.HTTPApiUseBasicAuth = *jsnHttpApiCfg.Use_basic_auth + } + if jsnHttpApiCfg.Basic_auth_realm != nil { + self.HTTPApiBasicAuthRealm = *jsnHttpApiCfg.Basic_auth_realm + } + if jsnHttpApiCfg.Htpasswd_file != nil { + self.HTTPApiHtpasswdFile = *jsnHttpApiCfg.Htpasswd_file + } + } + if jsnRALsCfg != nil { if jsnRALsCfg.Enabled != nil { self.RALsEnabled = *jsnRALsCfg.Enabled diff --git a/config/config_defaults.go b/config/config_defaults.go index eec70217b..b65eeaa6b 100644 --- a/config/config_defaults.go +++ b/config/config_defaults.go @@ -45,7 +45,7 @@ const CGRATES_CFG_JSON = ` "internal_ttl": "2m", // maximum duration to wait for internal connections before giving up "locking_timeout": "5s", // timeout internal locks to avoid deadlocks "cache_dump_dir": "", // cache dump for faster start (leave empty to disable) - "log_level": 6, // control the level of messages logged (0-emerg to 7-debug) + "log_level": 6, // control the level of messages logged (0-emerg to 7-debug) }, @@ -72,6 +72,13 @@ const CGRATES_CFG_JSON = ` }, +"http_api": { // HTTP API configuration + "use_basic_auth": false, // use basic authentication + "basic_auth_realm": "", // basic auth realm URL + "htpasswd_file": "" // basic auth htpasswd file location +}, + + "tariffplan_db": { // database used to store active tariff plan configuration "db_type": "redis", // tariffplan_db type: "db_host": "127.0.0.1", // tariffplan_db host address @@ -244,7 +251,7 @@ const CGRATES_CFG_JSON = ` "sms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from SMS unit to call duration in some billing systems) "mms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from MMS unit to call duration in some billing systems) "generic_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from GENERIC unit to call duration in some billing systems) - "cost_multiply_factor": 1, // multiply cost before export, eg: add VAT + "cost_multiply_factor": 1, // multiply cost before export, eg: add VAT "export_directory": "/var/spool/cgrates/cdre", // path where the exported CDRs will be placed "header_fields": [], // template of the exported header fields "content_fields": [ // template of the exported content fields diff --git a/config/config_json.go b/config/config_json.go index 317d03584..6246bbf7e 100644 --- a/config/config_json.go +++ b/config/config_json.go @@ -29,6 +29,7 @@ const ( GENERAL_JSN = "general" CACHE_JSN = "cache" LISTEN_JSN = "listen" + HTTP_API_JSN = "http_api" TPDB_JSN = "tariffplan_db" DATADB_JSN = "data_db" STORDB_JSN = "stor_db" @@ -118,6 +119,18 @@ func (self CgrJsonCfg) ListenJsonCfg() (*ListenJsonCfg, error) { return cfg, nil } +func (self CgrJsonCfg) HttpApiJsonCfg() (*HTTPApiJsonCfg, error) { + rawCfg, hasKey := self[HTTP_API_JSN] + if !hasKey { + return nil, nil + } + cfg := new(HTTPApiJsonCfg) + if err := json.Unmarshal(*rawCfg, cfg); err != nil { + return nil, err + } + return cfg, nil +} + func (self CgrJsonCfg) DbJsonCfg(section string) (*DbJsonCfg, error) { rawCfg, hasKey := self[section] if !hasKey { diff --git a/config/libconfig_json.go b/config/libconfig_json.go index 6cbd8e7fc..45b92efec 100644 --- a/config/libconfig_json.go +++ b/config/libconfig_json.go @@ -46,6 +46,13 @@ type ListenJsonCfg struct { Http *string } +// HTTP API config section +type HTTPApiJsonCfg struct { + Use_basic_auth *bool + Basic_auth_realm *string + Htpasswd_file *string +} + // Database config type DbJsonCfg struct { Db_type *string diff --git a/data/conf/cgrates/cgrates.json b/data/conf/cgrates/cgrates.json index c54cf81ee..6eddf6e3b 100644 --- a/data/conf/cgrates/cgrates.json +++ b/data/conf/cgrates/cgrates.json @@ -51,6 +51,12 @@ // }, +// "http_api" { // HTTP API configuration +// "use_basic_auth": false, // use basic authentication +// "basic_auth_realm": "", // basic auth realm URL +// "htpasswd_file": "", // basic auth htpasswd file location +// }, + // "tariffplan_db": { // database used to store active tariff plan configuration // "db_type": "redis", // tariffplan_db type: // "db_host": "127.0.0.1", // tariffplan_db host address @@ -199,7 +205,7 @@ // "sms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from SMS unit to call duration in some billing systems) // "mms_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from MMS unit to call duration in some billing systems) // "generic_usage_multiply_factor": 1, // multiply data usage before export (eg: convert from GENERIC unit to call duration in some billing systems) -// "cost_multiply_factor": 1, // multiply cost before export, eg: add VAT +// "cost_multiply_factor": 1, // multiply cost before export, eg: add VAT // "export_directory": "/var/spool/cgrates/cdre", // path where the exported CDRs will be placed // "header_fields": [], // template of the exported header fields // "content_fields": [ // template of the exported content fields diff --git a/glide.lock b/glide.lock index db06470c9..83f928653 100644 --- a/glide.lock +++ b/glide.lock @@ -1,6 +1,8 @@ -hash: da953ea34fabe4e21f4dde6344f3b7ab5a75e02122a1c17af5ea434058fc77fb -updated: 2016-09-06T20:33:01.649869367+02:00 +hash: cfda8a78a96bf7b3b471463d5ddf3330d6ae2089d5ac7c9a31b0f312e6518595 +updated: 2016-11-10T08:26:28.162167756-07:00 imports: +- name: github.com/abbot/go-http-auth + version: efc9484eee77263a11f158ef4f30fcc30298a942 - name: github.com/bit4bit/gami version: 3a7f98e7efce7ed7f22c2169b666910b8abb15dc - name: github.com/cenk/hub @@ -26,13 +28,17 @@ imports: - internal/parser/findutil - internal/parser/intfns - internal/parser/pathexpr - - internal/xconst - - internal/xsort + - literals/boollit + - literals/numlit + - literals/strlit - tree - tree/xmltree - tree/xmltree/xmlbuilder - tree/xmltree/xmlele - tree/xmltree/xmlnode + - xconst + - xfn + - xsort - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter @@ -76,6 +82,11 @@ imports: version: 5cd0f2b3b6cca8e3a0a4101821e41a73cb59bed6 subpackages: - codec +- name: golang.org/x/crypto + version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd + subpackages: + - bcrypt + - blowfish - name: golang.org/x/net version: 1358eff22f0dd0c54fc521042cc607f6ff4b531a subpackages: diff --git a/glide.yaml b/glide.yaml index 0d927866f..6a48d4ba8 100644 --- a/glide.yaml +++ b/glide.yaml @@ -39,3 +39,5 @@ import: - package: github.com/hashicorp/golang-lru - package: github.com/cgrates/aringo - package: github.com/bit4bit/gami +- package: github.com/abbot/go-http-auth + version: ~0.3.0 diff --git a/utils/server.go b/utils/server.go index 50f2d5509..44f4b3631 100644 --- a/utils/server.go +++ b/utils/server.go @@ -29,8 +29,8 @@ import ( "reflect" "time" + "github.com/abbot/go-http-auth" "github.com/cenk/rpc2" - "golang.org/x/net/websocket" ) import _ "net/http/pprof" @@ -140,17 +140,23 @@ func (s *Server) ServeGOB(addr string) { } } -func (s *Server) ServeHTTP(addr string) { +func handleRequest(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + w.Header().Set("Content-Type", "application/json") + res := NewRPCRequest(r.Body).Call() + io.Copy(w, res) +} + +func (s *Server) ServeHTTP(addr string, useBasicAuth bool, basicAuthRealm string, htpasswdFile string) { if s.rpcEnabled { - http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - w.Header().Set("Content-Type", "application/json") - res := NewRPCRequest(req.Body).Call() - io.Copy(w, res) - }) - http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) { - jsonrpc.ServeConn(ws) - })) + if useBasicAuth { + Logger.Info(fmt.Sprintf("Configuring CGRateS HTTP server to use basic auth (realm: %s, htpasswd: %s).", basicAuthRealm, htpasswdFile)) + secrets := auth.HtpasswdFileProvider(htpasswdFile) + authenticator := auth.NewBasicAuthenticator(basicAuthRealm, secrets) + http.HandleFunc("/jsonrpc", auth.JustCheck(authenticator, handleRequest)) + } else { + http.HandleFunc("/jsonrpc", handleRequest) + } s.httpEnabled = true } if !s.httpEnabled {