diff --git a/types.go b/types.go index bdf716a..f6a70e1 100644 --- a/types.go +++ b/types.go @@ -204,6 +204,7 @@ type Devices struct { type Config struct { User string Pass string + APIKey string URL string SSLCert [][]byte ErrorLog Logger diff --git a/unifi.go b/unifi.go index 29d0773..eea604d 100644 --- a/unifi.go +++ b/unifi.go @@ -35,9 +35,14 @@ var ( // Used to make additional, authenticated requests to the APIs. // Start here. func NewUnifi(config *Config) (*Unifi, error) { - jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) - if err != nil { - return nil, fmt.Errorf("creating cookiejar: %w", err) + var jar http.CookieJar + + var err error + if config.APIKey == "" { + jar, err = cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + if err != nil { + return nil, fmt.Errorf("creating cookiejar: %w", err) + } } u := newUnifi(config, jar) @@ -73,19 +78,25 @@ func newUnifi(config *Config, jar http.CookieJar) *Unifi { config.DebugLog = discardLogs } - u := &Unifi{ - Config: config, - Client: &http.Client{ - Timeout: config.Timeout, - Jar: jar, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: !config.VerifySSL, // nolint: gosec - }, + client := &http.Client{ + Timeout: config.Timeout, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: !config.VerifySSL, // nolint: gosec }, }, } + if config.APIKey == "" { + // old user/pass style use the cookie jar + client.Jar = jar + } + + u := &Unifi{ + Config: config, + Client: client, + } + if len(config.SSLCert) > 0 { u.fingerprints = make(fingerprints, len(config.SSLCert)) u.Client.Transport = &http.Transport{ @@ -154,6 +165,14 @@ func (u *Unifi) Logout() error { // check if this is a newer controller or not. If it is, we set new to true. // Setting new to true makes the path() method return different (new) paths. func (u *Unifi) checkNewStyleAPI() error { + if u.Config.APIKey != "" { + // we are using api keys so this must be the new style api + u.new = true + u.DebugLog("Using NEW UniFi controller API paths given an API Key was provided") + + return nil + } + var ( ctx = context.Background() cancel func() @@ -373,8 +392,13 @@ func (u *Unifi) do(req *http.Request) ([]byte, error) { } func (u *Unifi) setHeaders(req *http.Request, params string) { - // Add the saved CSRF header. - req.Header.Set("X-CSRF-Token", u.csrf) + if u.Config.APIKey != "" { + req.Header.Set("X-API-Key", u.Config.APIKey) + } else { + // Add the saved CSRF header. + req.Header.Set("X-CSRF-Token", u.csrf) + } + req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json; charset=utf-8") diff --git a/unifi_test.go b/unifi_test.go index 6f3d334..29b3f8a 100644 --- a/unifi_test.go +++ b/unifi_test.go @@ -1,8 +1,12 @@ package unifi // nolint: testpackage import ( + "encoding/json" + "fmt" "io" "net/http" + "net/http/httptest" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -25,6 +29,22 @@ func TestNewUnifi(t *testing.T) { a.Contains(err.Error(), "connection refused", "an invalid destination should produce a connection error.") } +func TestNewUnifiAPIKey(t *testing.T) { + t.Parallel() + a := assert.New(t) + u := "http://127.0.0.1:64431" + c := &Config{ + APIKey: "fakekey", + URL: u, + VerifySSL: false, + DebugLog: discardLogs, + } + authReq, err := NewUnifi(c) + a.NotNil(err) + a.EqualValues(u, authReq.URL) + a.Contains(err.Error(), "connection refused", "an invalid destination should produce a connection error.") +} + func TestUniReq(t *testing.T) { t.Parallel() a := assert.New(t) @@ -89,6 +109,68 @@ func TestUniReqPut(t *testing.T) { a.EqualValues(k, string(d), "PUT parameters improperly encoded") } +func TestUnifiIntegrationAPIKeyInjected(t *testing.T) { + t.Parallel() + a := assert.New(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("X-API-Key") == "fakekey" { + w.WriteHeader(http.StatusOK) + + return + } + + w.WriteHeader(http.StatusBadRequest) + })) + authReq := &Unifi{Client: &http.Client{}, Config: &Config{APIKey: "fakekey", URL: srv.URL, DebugLog: discardLogs}} + authResp, err := authReq.UniReqPost("/test", "") + a.Nil(err, "newrequest must not produce an error") + a.EqualValues("POST", authResp.Method, "with parameters the method must be POST") +} + +func TestUnifiIntegrationUserPassInjected(t *testing.T) { + t.Parallel() + a := assert.New(t) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.EqualFold(r.URL.Path, "/api/login") { + w.WriteHeader(http.StatusNotFound) + + return + } + + data, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Printf("error reading body:%v\n", err) + + return + } + + type userPass struct { + Username string `json:"username"` + Password string `json:"password"` + } + + var up userPass + + err = json.Unmarshal(data, &up) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Printf("error decoding body: %s: %s\n", string(data), err) + + return + } + + if strings.EqualFold(up.Username, "fakeuser") && strings.EqualFold(up.Password, "fakepass") { + w.WriteHeader(http.StatusOK) + } + + w.WriteHeader(http.StatusUnauthorized) + })) + authReq := &Unifi{Client: &http.Client{}, Config: &Config{User: "fakeuser", Pass: "fakepass", URL: srv.URL, DebugLog: discardLogs}} + err := authReq.Login() + a.Nil(err, "user/pass login must not produce an error") +} + /* NOT DONE: OPEN web server, check parameters posted, more. These tests are incomplete. a.EqualValues(`{"username": "user1","password": "pass2"}`, string(post_params), "user/pass json parameters improperly encoded")