Skip to content

Commit

Permalink
fix: missing Plex API metrics (#141)
Browse files Browse the repository at this point in the history
* fix: missing Plex API metrics

* fix: missing Plex API metrics

* fix: missing Plex API metrics

* fix: missing Plex API metrics
  • Loading branch information
clambin authored Jul 9, 2023
1 parent 3e19175 commit 7c6046e
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 227 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- master
- plexauth
- fix
paths-ignore:
- 'assets/**'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches-ignore:
- master
- plexauth
- fix
pull_request_target:

jobs:
Expand Down
4 changes: 4 additions & 0 deletions cmd/qplex/qplex.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ func getAuthToken(cmd *cobra.Command, _ []string) {
"qplex",
cmd.Version,
viper.GetString("url"),
nil,
)
if authToken := viper.GetString("auth.token"); authToken != "" {
c.SetAuthToken(authToken)
Expand All @@ -125,6 +126,7 @@ func getViews(cmd *cobra.Command, _ []string) {
"qplex",
cmd.Version,
viper.GetString("url"),
nil,
)

views, err := qplex.GetViews(ctx, c, tokens, viper.GetBool("views.reverse"))
Expand Down Expand Up @@ -152,6 +154,7 @@ func getTokens(ctx context.Context, server bool) ([]string, error) {
"qplex",
BuildVersion,
viper.GetString("url"),
nil,
)
if authToken := viper.GetString("auth.token"); authToken != "" {
c.SetAuthToken(authToken)
Expand Down Expand Up @@ -254,6 +257,7 @@ func getSessions(cmd *cobra.Command, _ []string) {
"qplex",
cmd.Version,
viper.GetString("url"),
nil,
)
if authToken := viper.GetString("auth.token"); authToken != "" {
c.SetAuthToken(authToken)
Expand Down
2 changes: 1 addition & 1 deletion collectors/plex/plex.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type Config struct {
func NewCollector(version, url, username, password string) *Collector {
r := httpclient.NewRoundTripper(httpclient.WithMetrics("mediamon", "", "plex"))
return &Collector{
API: plex.New(username, password, "github.com/clambin/mediamon", version, url),
API: plex.New(username, password, "github.com/clambin/mediamon", version, url, r),
IPLocator: iplocator.New(),
url: url,
transport: r,
Expand Down
79 changes: 39 additions & 40 deletions pkg/mediaclient/plex/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,48 @@ import (

const authURL = "https://plex.tv/users/sign_in.xml"

// SetAuthToken sets the AuthToken
func (c *Client) SetAuthToken(s string) {
c.plexAuth.lock.Lock()
defer c.plexAuth.lock.Unlock()
c.plexAuth.authToken = s
var _ http.RoundTripper = &authenticator{}

type authenticator struct {
httpClient *http.Client
username string
password string
authURL string
product string
version string
next http.RoundTripper
lock sync.Mutex
authToken string
}

// GetAuthToken logs into plex.tv and returns the current authToken.
func (c *Client) GetAuthToken(ctx context.Context) (string, error) {
err := c.plexAuth.authenticate(ctx)
if err != nil {
return "", err
func (a *authenticator) RoundTrip(request *http.Request) (*http.Response, error) {
if err := a.authenticate(request.Context()); err != nil {
return nil, err
}

c.plexAuth.lock.Lock()
defer c.plexAuth.lock.Unlock()
return c.plexAuth.authToken, nil
request.Header.Add("X-Plex-Token", a.authToken)
return a.next.RoundTrip(request)
}

var _ http.RoundTripper = &Authenticator{}

type Authenticator struct {
HTTPClient *http.Client
Username string
Password string
AuthURL string
Product string
Version string
Next http.RoundTripper
authToken string
lock sync.Mutex
// SetAuthToken sets the AuthToken
func (a *authenticator) SetAuthToken(s string) {
a.lock.Lock()
defer a.lock.Unlock()
a.authToken = s
}

func (a *Authenticator) RoundTrip(request *http.Request) (*http.Response, error) {
if err := a.authenticate(request.Context()); err != nil {
return nil, err
// GetAuthToken logs into plex.tv and returns the current authToken.
func (a *authenticator) GetAuthToken(ctx context.Context) (string, error) {
err := a.authenticate(ctx)
if err != nil {
return "", err
}
request.Header.Add("X-Plex-Token", a.authToken)

return a.Next.RoundTrip(request)
a.lock.Lock()
defer a.lock.Unlock()
return a.authToken, nil
}

func (a *Authenticator) authenticate(ctx context.Context) error {
func (a *authenticator) authenticate(ctx context.Context) error {
a.lock.Lock()
defer a.lock.Unlock()

Expand All @@ -64,7 +63,7 @@ func (a *Authenticator) authenticate(ctx context.Context) error {
}

req, _ := a.makeAuthRequest(ctx)
resp, err := a.HTTPClient.Do(req)
resp, err := a.httpClient.Do(req)
if err != nil {
return err
}
Expand All @@ -79,16 +78,16 @@ func (a *Authenticator) authenticate(ctx context.Context) error {
return err
}

func (a *Authenticator) makeAuthRequest(ctx context.Context) (*http.Request, error) {
func (a *authenticator) makeAuthRequest(ctx context.Context) (*http.Request, error) {
v := make(url.Values)
v.Set("user[login]", a.Username)
v.Set("user[password]", a.Password)
v.Set("user[login]", a.username)
v.Set("user[password]", a.password)

req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.AuthURL, bytes.NewBufferString(v.Encode()))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.authURL, bytes.NewBufferString(v.Encode()))
if err == nil {
req.Header.Add("X-Plex-Product", a.Product)
req.Header.Add("X-Plex-Version", a.Version)
req.Header.Add("X-Plex-Client-Identifier", a.Product+"-v"+a.Version)
req.Header.Add("X-Plex-Product", a.product)
req.Header.Add("X-Plex-Version", a.version)
req.Header.Add("X-Plex-Client-Identifier", a.product+"-v"+a.version)
}
return req, err
}
Expand Down
82 changes: 53 additions & 29 deletions pkg/mediaclient/plex/authenticator_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package plex_test
package plex

import (
"context"
"github.com/clambin/mediamon/v2/pkg/mediaclient/plex"
"github.com/clambin/mediamon/v2/pkg/mediaclient/plex/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
Expand All @@ -11,31 +11,73 @@ import (
)

func TestAuthenticator_RoundTrip(t *testing.T) {
authServer := httptest.NewServer(http.HandlerFunc(plexAuthHandler))
authServer := httptest.NewServer(http.HandlerFunc(testutil.AuthHandler))

server := httptest.NewServer(testutil.WithToken("some_token", testutil.Handler))
defer server.Client()

c := New("user@example.com", "somepassword", "", "", server.URL, nil)
c.authenticator.authURL = authServer.URL

resp, err := c.GetIdentity(context.Background())
require.NoError(t, err)
assert.Equal(t, Identity{
Claimed: true,
MachineIdentifier: "SomeUUID",
Version: "SomeVersion",
}, resp)

c.SetAuthToken("")
c.HTTPClient.Transport.(*authenticator).password = "badpassword"

_, err = c.GetIdentity(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "plex auth: 403 Forbidden")

authServer.Close()
_, err = c.GetIdentity(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "connect: connection refused")

}

func TestAuthenticator_Custom_RoundTripper(t *testing.T) {
authServer := httptest.NewServer(http.HandlerFunc(testutil.AuthHandler))
defer authServer.Close()

server := httptest.NewServer(authenticated("some_token", plexHandler))
server := httptest.NewServer(testutil.WithToken("some_token", testutil.Handler))
defer server.Client()

c := plex.New("user@example.com", "somepassword", "", "", server.URL)
c.HTTPClient.Transport.(*plex.Authenticator).AuthURL = authServer.URL
c := New("user@example.com", "somepassword", "", "", server.URL, &dummyRoundTripper{next: http.DefaultTransport})
c.authenticator.authURL = authServer.URL

resp, err := c.GetIdentity(context.Background())
require.NoError(t, err)
assert.Equal(t, plex.Identity{
assert.Equal(t, Identity{
Claimed: true,
MachineIdentifier: "SomeUUID",
Version: "SomeVersion",
}, resp)

c.SetAuthToken("")
c.HTTPClient.Transport.(*plex.Authenticator).Password = "badpassword"
c.HTTPClient.Transport.(*authenticator).password = "badpassword"

_, err = c.GetIdentity(context.Background())
assert.Error(t, err)
assert.Contains(t, err.Error(), "plex auth: 403 Forbidden")
}

var _ http.RoundTripper = &dummyRoundTripper{}

type dummyRoundTripper struct {
next http.RoundTripper
}

func (d *dummyRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
request.Header.Set("X-Dummy", "foo")
return d.next.RoundTrip(request)
}

func TestClient_GetAuthToken(t *testing.T) {
type fields struct {
AuthToken string
Expand Down Expand Up @@ -67,13 +109,13 @@ func TestClient_GetAuthToken(t *testing.T) {
},
}

authServer := httptest.NewServer(http.HandlerFunc(plexAuthHandler))
authServer := httptest.NewServer(http.HandlerFunc(testutil.AuthHandler))
defer authServer.Close()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := plex.New(tt.fields.UserName, tt.fields.Password, "", "", "")
c.HTTPClient.Transport.(*plex.Authenticator).AuthURL = authServer.URL
c := New(tt.fields.UserName, tt.fields.Password, "", "", "", nil)
c.authenticator.authURL = authServer.URL
if tt.fields.AuthToken != "" {
c.SetAuthToken(tt.fields.AuthToken)
}
Expand All @@ -86,21 +128,3 @@ func TestClient_GetAuthToken(t *testing.T) {
})
}
}

func authenticated(token string, next http.HandlerFunc) http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
if request.Header.Get("X-Plex-Token") != token {
writer.WriteHeader(http.StatusForbidden)
return
}
next(writer, request)
}
}

func plexHandler(w http.ResponseWriter, req *http.Request) {
if response, ok := plexResponses[req.URL.Path]; ok {
_, _ = w.Write([]byte(response))
} else {
http.Error(w, "endpoint not implemented: "+req.URL.Path, http.StatusNotFound)
}
}
10 changes: 2 additions & 8 deletions pkg/mediaclient/plex/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@ package plex_test

import (
"context"
"github.com/clambin/mediamon/v2/pkg/mediaclient/plex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"testing"
)

func TestPlexClient_GetIdentity(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(plexHandler))
defer testServer.Close()

c := plex.New("user@example.com", "somepassword", "", "", testServer.URL)
c.HTTPClient.Transport = http.DefaultTransport
c, s := makeClientAndServer(nil)
defer s.Close()

identity, err := c.GetIdentity(context.Background())
require.NoError(t, err)
Expand Down
48 changes: 48 additions & 0 deletions pkg/mediaclient/plex/internal/testutil/authserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package testutil

import (
"io"
"net/http"
"net/url"
)

func AuthHandler(w http.ResponseWriter, req *http.Request) {
defer func() {
_ = req.Body.Close()
}()

body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}

auth, err := url.PathUnescape(string(body))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if auth != `user[login]=user@example.com&user[password]=somepassword` {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(authResponse))
}

const (
authResponse = `<?xml version="1.0" encoding="UTF-8"?>
<user email="user@example.com" id="1" uuid="1" username="user" authenticationToken="some_token" authToken="some_token">
<subscription active="0" status="Inactive" plan=""></subscription>
<entitlements all="0"></entitlements>
<profile_settings/>
<providers></providers>
<services></services>
<username>user</username>
<email>user@example.com</email>
<joined-at type="datetime">2000-01-01 00:00:00 UTC</joined-at>
<authentication-token>some_token</authentication-token>
</user>`
)
Loading

0 comments on commit 7c6046e

Please sign in to comment.