| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package handlers
- import (
- "context"
- "encoding/base64"
- "encoding/json"
- "log/slog"
- "net/http"
- "net/http/httptest"
- "testing"
- "github.com/stretchr/testify/assert"
- "github.com/mk6i/open-oscar-server/server/webapi/middleware"
- "github.com/mk6i/open-oscar-server/state"
- )
- // testOSCARConfig implements OSCARConfig with fixed addresses.
- type testOSCARConfig struct {
- sslAvailable bool
- }
- func (c testOSCARConfig) GetBOSAddress() (string, int) { return "bos.example.com", 5190 }
- func (c testOSCARConfig) GetSSLBOSAddress() (string, int) { return "ssl.example.com", 5193 }
- func (c testOSCARConfig) IsSSLAvailable() bool { return c.sslAvailable }
- // bridgeRequest builds a startOSCARSession request carrying the API key the
- // middleware would have put on the context.
- func bridgeRequest(query string, apiKey *state.WebAPIKey) *http.Request {
- req := httptest.NewRequest(http.MethodGet, "/aim/startOSCARSession?"+query, nil)
- if apiKey != nil {
- req = req.WithContext(context.WithValue(req.Context(), middleware.ContextKeyAPIKey, apiKey))
- }
- return req
- }
- // bridgeData is the data object of a successful startOSCARSession response.
- type bridgeData struct {
- Response struct {
- StatusCode int `json:"statusCode"`
- Data struct {
- Host string `json:"host"`
- Port int `json:"port"`
- Cookie string `json:"cookie"`
- TLSCertName string `json:"tlsCertName"`
- } `json:"data"`
- } `json:"response"`
- }
- func TestOSCARBridgeHandler_StartOSCARSession(t *testing.T) {
- validToken := base64.URLEncoding.EncodeToString(signedCookieFor("testuser"))
- unrestrictedKey := &state.WebAPIKey{DevID: "dev123"}
- tests := []struct {
- name string
- query string
- apiKey *state.WebAPIKey
- sslAvailable bool
- expectedCode int
- checkBody func(t *testing.T, body string)
- }{
- {
- // No tlsCertName, which is how the client reads "connect in the clear".
- name: "Success_Plaintext",
- query: "a=" + validToken,
- apiKey: unrestrictedKey,
- expectedCode: http.StatusOK,
- checkBody: func(t *testing.T, body string) {
- got := decodeBridgeData(t, body)
- assert.Equal(t, 200, got.Response.StatusCode)
- assert.Equal(t, "bos.example.com", got.Response.Data.Host)
- assert.Equal(t, 5190, got.Response.Data.Port)
- assert.Empty(t, got.Response.Data.TLSCertName)
- },
- },
- {
- name: "Success_TLS",
- query: "a=" + validToken + "&useTLS=1",
- apiKey: unrestrictedKey,
- sslAvailable: true,
- expectedCode: http.StatusOK,
- checkBody: func(t *testing.T, body string) {
- got := decodeBridgeData(t, body)
- assert.Equal(t, "ssl.example.com", got.Response.Data.Host)
- assert.Equal(t, 5193, got.Response.Data.Port)
- // The certificate is issued to the host the client is sent to.
- assert.Equal(t, "ssl.example.com", got.Response.Data.TLSCertName)
- },
- },
- {
- // Encryption the server cannot provide degrades to a plaintext host
- // rather than failing the handoff.
- name: "TLSRequestedButUnavailable_DegradesToPlaintext",
- query: "a=" + validToken + "&useTLS=true",
- apiKey: unrestrictedKey,
- sslAvailable: false,
- expectedCode: http.StatusOK,
- checkBody: func(t *testing.T, body string) {
- got := decodeBridgeData(t, body)
- assert.Equal(t, "bos.example.com", got.Response.Data.Host)
- assert.Empty(t, got.Response.Data.TLSCertName)
- },
- },
- {
- name: "Error_MissingToken",
- query: "",
- apiKey: unrestrictedKey,
- expectedCode: http.StatusUnauthorized,
- checkBody: func(t *testing.T, body string) {
- assert.Contains(t, body, "authentication token required")
- },
- },
- {
- name: "Error_TokenNotBase64",
- query: "a=not!valid!base64",
- apiKey: unrestrictedKey,
- expectedCode: http.StatusUnauthorized,
- checkBody: func(t *testing.T, body string) {
- assert.Contains(t, body, "invalid or expired token")
- },
- },
- {
- // A well-formed token the baker refuses to crack: wrong signature or
- // past its expiry.
- name: "Error_TokenFailsSignatureCheck",
- query: "a=" + base64.URLEncoding.EncodeToString([]byte("forged")),
- apiKey: unrestrictedKey,
- expectedCode: http.StatusUnauthorized,
- checkBody: func(t *testing.T, body string) {
- assert.Contains(t, body, "invalid or expired token")
- },
- },
- {
- name: "Error_NoAPIKeyOnContext",
- query: "a=" + validToken,
- apiKey: nil,
- expectedCode: http.StatusInternalServerError,
- checkBody: func(t *testing.T, body string) {
- assert.Contains(t, body, "internal server error")
- },
- },
- {
- name: "Error_APIKeyLacksBridgeCapability",
- query: "a=" + validToken,
- apiKey: &state.WebAPIKey{DevID: "dev123", Capabilities: []string{"presence"}},
- expectedCode: http.StatusForbidden,
- checkBody: func(t *testing.T, body string) {
- assert.Contains(t, body, "OSCAR bridge not enabled")
- },
- },
- {
- name: "Success_APIKeyGrantsBridgeCapability",
- query: "a=" + validToken,
- apiKey: &state.WebAPIKey{DevID: "dev123", Capabilities: []string{"presence", "oscar_bridge"}},
- expectedCode: http.StatusOK,
- checkBody: func(t *testing.T, body string) {
- assert.Equal(t, 200, decodeBridgeData(t, body).Response.StatusCode)
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- handler := &OSCARBridgeHandler{
- OSCARAuthService: &testAuthService{crackCookie: crackSignedCookie},
- Config: testOSCARConfig{sslAvailable: tt.sslAvailable},
- Logger: slog.Default(),
- }
- rr := httptest.NewRecorder()
- handler.StartOSCARSession(rr, bridgeRequest(tt.query, tt.apiKey))
- assert.Equal(t, tt.expectedCode, rr.Code)
- tt.checkBody(t, rr.Body.String())
- })
- }
- }
- // The token arrives URL-safe, the way clientLogin minted it, and goes back out in
- // standard base64, the alphabet the client decodes the sign-on cookie with. The
- // cookie bytes here encode differently under each.
- func TestOSCARBridgeHandler_StartOSCARSession_ReencodesCookie(t *testing.T) {
- rawCookie := []byte{0xff, 0xef, 0xbe}
- urlSafe := base64.URLEncoding.EncodeToString(rawCookie)
- standard := base64.StdEncoding.EncodeToString(rawCookie)
- assert.NotEqual(t, urlSafe, standard, "test cookie must distinguish the two alphabets")
- var cracked []byte
- handler := &OSCARBridgeHandler{
- OSCARAuthService: &testAuthService{
- crackCookie: func(authCookie []byte) (state.ServerCookie, error) {
- cracked = authCookie
- return state.ServerCookie{ScreenName: "testuser"}, nil
- },
- },
- Config: testOSCARConfig{},
- Logger: slog.Default(),
- }
- rr := httptest.NewRecorder()
- handler.StartOSCARSession(rr, bridgeRequest("a="+urlSafe, &state.WebAPIKey{DevID: "dev123"}))
- assert.Equal(t, http.StatusOK, rr.Code)
- assert.Equal(t, rawCookie, cracked, "the baker sees the decoded cookie")
- assert.Equal(t, standard, decodeBridgeData(t, rr.Body.String()).Response.Data.Cookie)
- }
- func decodeBridgeData(t *testing.T, body string) bridgeData {
- t.Helper()
- got := bridgeData{}
- assert.NoError(t, json.Unmarshal([]byte(body), &got))
- return got
- }
|