Skip to content

Commit

Permalink
Merge pull request #145 from unpoller/apikey-support
Browse files Browse the repository at this point in the history
add support for api-key auth
  • Loading branch information
platinummonkey authored Jan 10, 2025
2 parents 00fd95b + 6012bfe commit a121cdf
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 15 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
67 changes: 52 additions & 15 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 @@ -115,10 +126,18 @@ func (u *Unifi) verifyPeerCertificate(certs [][]byte, _ [][]*x509.Certificate) e

// Login is a helper method. It can be called to grab a new authentication cookie.
func (u *Unifi) Login() error {
loginPath := APIStatusPath
params := ""

if u.Config.APIKey == "" {
params = fmt.Sprintf(`{"username":"%s","password":"%s"}`, u.User, u.Pass)
loginPath = APILoginPath
}

start := time.Now()

// magic login.
req, err := u.UniReq(APILoginPath, fmt.Sprintf(`{"username":"%s","password":"%s"}`, u.User, u.Pass))
req, err := u.UniReq(loginPath, params)
if err != nil {
return err
}
Expand All @@ -143,6 +162,11 @@ func (u *Unifi) Login() error {

// Logout closes the current session.
func (u *Unifi) Logout() error {
if u.Config.APIKey != "" {
// no need to logout on api-key auth
return nil
}

// a post is needed for logout
_, err := u.PostJSON(APILogoutPath)

Expand All @@ -154,6 +178,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 +405,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 a121cdf

Please sign in to comment.