Skip to content

Commit

Permalink
add support for api-key auth
Browse files Browse the repository at this point in the history
  • Loading branch information
platinummonkey committed Jan 10, 2025
1 parent 00fd95b commit b4541de
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 14 deletions.
1 change: 1 addition & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ type Devices struct {
type Config struct {
User string
Pass string
APIKey string
URL string
SSLCert [][]byte
ErrorLog Logger
Expand Down
52 changes: 38 additions & 14 deletions unifi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")

Expand Down
82 changes: 82 additions & 0 deletions unifi_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit b4541de

Please sign in to comment.