diff --git a/backend/auth/server.go b/backend/auth/server.go index b3721e2e..f099784e 100644 --- a/backend/auth/server.go +++ b/backend/auth/server.go @@ -15,41 +15,74 @@ type Handler struct { key, secret string } +type OpenIDTokenType struct { +} + +type SFURequest struct { + Room string `json:"room"` + OpenIDToken OpenIDTokenType `json:"openid_token"` + DeviceID string `json:"device_id"` + RemoveMeUserID string `json:"remove_me_user_id"` // we'll get this from OIDC +} + +type SFUResponse struct { + URL string `json:"url"` + JWT string `json:"jwt"` +} + func (h *Handler) handle(w http.ResponseWriter, r *http.Request) { log.Printf("Request from %s", r.RemoteAddr) // Set the CORS headers w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") - w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") + w.Header().Set("Access-Control-Allow-Methods", "POST") + w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token") // Handle preflight request (CORS) if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return + } else if r.Method == "POST" { + var body SFURequest + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + log.Printf("Error decoding JSON: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if body.Room == "" { + log.Printf("Request missing room") + w.WriteHeader(http.StatusBadRequest) + return + } + + token, err := getJoinToken(h.key, h.secret, body.Room, body.RemoveMeUserID+":"+body.DeviceID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + res := SFUResponse{URL: "http://localhost:7880/", JWT: token} + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(res) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) } - roomName := r.URL.Query().Get("roomName") - name := r.URL.Query().Get("name") - identity := r.URL.Query().Get("identity") + /* + roomName := r.URL.Query().Get("roomName") + name := r.URL.Query().Get("name") + identity := r.URL.Query().Get("identity") - log.Printf("roomName: %s, name: %s, identity: %s", roomName, name, identity) + log.Printf("roomName: %s, name: %s, identity: %s", roomName, name, identity) - if roomName == "" || name == "" || identity == "" { - w.WriteHeader(http.StatusBadRequest) - return - } - - token, err := getJoinToken(h.key, h.secret, roomName, identity, name) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - res := Response{token} - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(res) + if roomName == "" || name == "" || identity == "" { + w.WriteHeader(http.StatusBadRequest) + return + } + */ } func main() { @@ -68,15 +101,11 @@ func main() { secret: secret, } - http.HandleFunc("/token", handler.handle) + http.HandleFunc("/sfu/get", handler.handle) log.Fatal(http.ListenAndServe(":8080", nil)) } -type Response struct { - Token string `json:"accessToken"` -} - -func getJoinToken(apiKey, apiSecret, room, identity, name string) (string, error) { +func getJoinToken(apiKey, apiSecret, room, identity string) (string, error) { at := auth.NewAccessToken(apiKey, apiSecret) canPublish := true @@ -91,8 +120,7 @@ func getJoinToken(apiKey, apiSecret, room, identity, name string) (string, error at.AddGrant(grant). SetIdentity(identity). - SetValidFor(time.Hour). - SetName(name) + SetValidFor(time.Hour) return at.ToJWT() } diff --git a/config/element_io_preview.json b/config/element_io_preview.json index 30c2cc80..cb6fc088 100644 --- a/config/element_io_preview.json +++ b/config/element_io_preview.json @@ -6,8 +6,7 @@ } }, "livekit": { - "server_url": "wss://sfu.call.element.dev", - "jwt_service_url": "https://voip-sip-poc.element.io/lk/jwt_service" + "livekit_service_url": "https://lk-jwt-service.lab.element.dev" }, "posthog": { "api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU", diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 9de2ab48..ba1b217b 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -142,8 +142,7 @@ export const ClientProvider: FC = ({ children }) => { const foci = livekit ? [ { - url: livekit.server_url, - jwtServiceUrl: livekit.jwt_service_url, + livekitServiceUrl: livekit.livekit_service_url, }, ] : undefined; diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 1cee00fa..f878ec5e 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -55,10 +55,8 @@ export interface ConfigOptions { // Describes the LiveKit configuration to be used. livekit?: { - // The LiveKit server URL to connect to. - server_url: string; - // The link to the service that generates JWT tokens to join LiveKit rooms. - jwt_service_url: string; + // The link to the service that returns a livekit url and token to use it + livekit_service_url: string; }; /** diff --git a/src/livekit/OpenIDLoader.tsx b/src/livekit/OpenIDLoader.tsx new file mode 100644 index 00000000..9e68d311 --- /dev/null +++ b/src/livekit/OpenIDLoader.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from "react"; +import { logger } from "matrix-js-sdk/src/logger"; + +import { + OpenIDClientParts, + SFUConfig, + getSFUConfigWithOpenID, +} from "./openIDSFU"; +import { ErrorView, LoadingView } from "../FullScreenView"; + +interface Props { + client: OpenIDClientParts; + livekitServiceURL: string; + roomName: string; + children: ReactNode; +} + +const SFUConfigContext = createContext(undefined); + +export const useSFUConfig = () => useContext(SFUConfigContext); + +export function OpenIDLoader({ + client, + livekitServiceURL, + roomName, + children, +}: Props) { + const [state, setState] = useState< + SFUConfigLoading | SFUConfigLoaded | SFUConfigFailed + >({ kind: "loading" }); + + useEffect(() => { + (async () => { + try { + const result = await getSFUConfigWithOpenID( + client, + livekitServiceURL, + roomName + ); + setState({ kind: "loaded", sfuConfig: result }); + } catch (e) { + logger.error("Failed to fetch SFU config: ", e); + setState({ kind: "failed", error: e }); + } + })(); + }, [client, livekitServiceURL, roomName]); + + switch (state.kind) { + case "loading": + return ; + case "failed": + return ; + case "loaded": + return ( + + {children} + + ); + } +} + +type SFUConfigLoading = { + kind: "loading"; +}; + +type SFUConfigLoaded = { + kind: "loaded"; + sfuConfig: SFUConfig; +}; + +type SFUConfigFailed = { + kind: "failed"; + error: Error; +}; diff --git a/src/livekit/openIDSFU.ts b/src/livekit/openIDSFU.ts new file mode 100644 index 00000000..89a5d4f3 --- /dev/null +++ b/src/livekit/openIDSFU.ts @@ -0,0 +1,54 @@ +/* +Copyright 2023 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { MatrixClient } from "matrix-js-sdk"; +import { logger } from "matrix-js-sdk/src/logger"; + +export interface SFUConfig { + url: string; + jwt: string; +} + +// The bits we need from MatrixClient +export type OpenIDClientParts = Pick< + MatrixClient, + "getOpenIdToken" | "getDeviceId" +>; + +export async function getSFUConfigWithOpenID( + client: OpenIDClientParts, + livekitServiceURL: string, + roomName: string +): Promise { + const openIdToken = await client.getOpenIdToken(); + logger.debug("Got openID token", openIdToken); + + const res = await fetch(livekitServiceURL + "/sfu/get", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room: roomName, + openid_token: openIdToken, + device_id: client.getDeviceId(), + }), + }); + if (!res.ok) { + throw new Error("SFO Config fetch failed with status code " + res.status); + } + return await res.json(); +} diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 22767405..c35f4f46 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -1,8 +1,9 @@ import { Room, RoomOptions } from "livekit-client"; -import { useLiveKitRoom, useToken } from "@livekit/components-react"; +import { useLiveKitRoom } from "@livekit/components-react"; import { useMemo } from "react"; import { defaultLiveKitOptions } from "./options"; +import { SFUConfig } from "./openIDSFU"; export type UserChoices = { audio?: DeviceChoices; @@ -14,29 +15,10 @@ export type DeviceChoices = { enabled: boolean; }; -export type LiveKitConfig = { - sfuUrl: string; - jwtUrl: string; - roomName: string; - userDisplayName: string; - userIdentity: string; -}; - export function useLiveKit( userChoices: UserChoices, - config: LiveKitConfig + sfuConfig: SFUConfig ): Room | undefined { - const tokenOptions = useMemo( - () => ({ - userInfo: { - name: config.userDisplayName, - identity: config.userIdentity, - }, - }), - [config.userDisplayName, config.userIdentity] - ); - const token = useToken(config.jwtUrl, config.roomName, tokenOptions); - const roomOptions = useMemo((): RoomOptions => { const options = defaultLiveKitOptions; options.videoCaptureDefaults = { @@ -51,8 +33,8 @@ export function useLiveKit( }, [userChoices.video, userChoices.audio]); const { room } = useLiveKitRoom({ - token, - serverUrl: config.sfuUrl, + token: sfuConfig.jwt, + serverUrl: sfuConfig.url, audio: userChoices.audio?.enabled ?? false, video: userChoices.video?.enabled ?? false, options: roomOptions, diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 76a4c414..1b23fcf9 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -28,13 +28,14 @@ import { useGroupCall } from "./useGroupCall"; import { ErrorView, FullScreenView } from "../FullScreenView"; import { LobbyView } from "./LobbyView"; import { MatrixInfo } from "./VideoPreview"; -import { ActiveCall } from "./InCallView"; import { CallEndedView } from "./CallEndedView"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { useProfile } from "../profile/useProfile"; import { UserChoices } from "../livekit/useLiveKit"; import { findDeviceByName } from "../media-utils"; +import { OpenIDLoader } from "../livekit/OpenIDLoader"; +import { ActiveCall } from "./InCallView"; declare global { interface Window { @@ -218,21 +219,39 @@ export function GroupCallView({ undefined ); + const [livekitServiceURL, setLivekitServiceURL] = useState< + string | undefined + >(groupCall.foci[0]?.livekitServiceUrl); + + useEffect(() => { + setLivekitServiceURL(groupCall.foci[0]?.livekitServiceUrl); + }, [setLivekitServiceURL, groupCall]); + + if (!livekitServiceURL) { + return ; + } + if (error) { return ; } else if (state === GroupCallState.Entered && userChoices) { return ( - + livekitServiceURL={livekitServiceURL} + roomName={matrixInfo.roomName} + > + + ); } else if (left) { // The call ended view is shown for two reasons: prompting guests to create diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index d691e867..98a3a63b 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -83,6 +83,7 @@ import { UserChoices, useLiveKit } from "../livekit/useLiveKit"; import { useMediaDevices } from "../livekit/useMediaDevices"; import { useFullscreen } from "./useFullscreen"; import { useLayoutStates } from "../video-grid/Layout"; +import { useSFUConfig } from "../livekit/OpenIDLoader"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -90,18 +91,13 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // For now we can disable screensharing in Safari. const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); -interface ActiveCallProps extends Omit { +export interface ActiveCallProps extends Omit { userChoices: UserChoices; } export function ActiveCall(props: ActiveCallProps) { - const livekitRoom = useLiveKit(props.userChoices, { - sfuUrl: props.groupCall.foci[0]!.url, - jwtUrl: `${props.groupCall.foci[0]!.jwtServiceUrl}/token`, - roomName: props.matrixInfo.roomName, - userDisplayName: props.matrixInfo.displayName, - userIdentity: `${props.client.getUserId()}:${props.client.getDeviceId()}`, - }); + const sfuConfig = useSFUConfig(); + const livekitRoom = useLiveKit(props.userChoices, sfuConfig); return ( livekitRoom && ( @@ -112,7 +108,7 @@ export function ActiveCall(props: ActiveCallProps) { ); } -interface Props { +export interface InCallViewProps { client: MatrixClient; groupCall: GroupCall; livekitRoom: Room; @@ -134,7 +130,7 @@ export function InCallView({ hideHeader, matrixInfo, otelGroupCallMembership, -}: Props) { +}: InCallViewProps) { const { t } = useTranslation(); usePreventScroll();