Merge remote-tracking branch 'upstream/dbkr/openid' into SimonBrandner/feat/url

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2023-07-04 17:43:40 +02:00
commit ce1671a117
No known key found for this signature in database
GPG key ID: D1D45825D60C24D2
9 changed files with 252 additions and 81 deletions

View file

@ -15,41 +15,74 @@ type Handler struct {
key, secret string 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) { func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
log.Printf("Request from %s", r.RemoteAddr) log.Printf("Request from %s", r.RemoteAddr)
// Set the CORS headers // Set the CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*") 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-Methods", "POST")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token")
// Handle preflight request (CORS) // Handle preflight request (CORS)
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return 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") roomName := r.URL.Query().Get("roomName")
identity := r.URL.Query().Get("identity") 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 == "" { if roomName == "" || name == "" || identity == "" {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return 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)
} }
func main() { func main() {
@ -68,15 +101,11 @@ func main() {
secret: secret, secret: secret,
} }
http.HandleFunc("/token", handler.handle) http.HandleFunc("/sfu/get", handler.handle)
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
type Response struct { func getJoinToken(apiKey, apiSecret, room, identity string) (string, error) {
Token string `json:"accessToken"`
}
func getJoinToken(apiKey, apiSecret, room, identity, name string) (string, error) {
at := auth.NewAccessToken(apiKey, apiSecret) at := auth.NewAccessToken(apiKey, apiSecret)
canPublish := true canPublish := true
@ -91,8 +120,7 @@ func getJoinToken(apiKey, apiSecret, room, identity, name string) (string, error
at.AddGrant(grant). at.AddGrant(grant).
SetIdentity(identity). SetIdentity(identity).
SetValidFor(time.Hour). SetValidFor(time.Hour)
SetName(name)
return at.ToJWT() return at.ToJWT()
} }

View file

@ -6,8 +6,7 @@
} }
}, },
"livekit": { "livekit": {
"server_url": "wss://sfu.call.element.dev", "livekit_service_url": "https://lk-jwt-service.lab.element.dev"
"jwt_service_url": "https://voip-sip-poc.element.io/lk/jwt_service"
}, },
"posthog": { "posthog": {
"api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU", "api_key": "phc_rXGHx9vDmyEvyRxPziYtdVIv0ahEv8A9uLWFcCi1WcU",

View file

@ -142,8 +142,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
const foci = livekit const foci = livekit
? [ ? [
{ {
url: livekit.server_url, livekitServiceUrl: livekit.livekit_service_url,
jwtServiceUrl: livekit.jwt_service_url,
}, },
] ]
: undefined; : undefined;

View file

@ -55,10 +55,8 @@ export interface ConfigOptions {
// Describes the LiveKit configuration to be used. // Describes the LiveKit configuration to be used.
livekit?: { livekit?: {
// The LiveKit server URL to connect to. // The link to the service that returns a livekit url and token to use it
server_url: string; livekit_service_url: string;
// The link to the service that generates JWT tokens to join LiveKit rooms.
jwt_service_url: string;
}; };
/** /**

View file

@ -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<SFUConfig>(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 <LoadingView />;
case "failed":
return <ErrorView error={state.error} />;
case "loaded":
return (
<SFUConfigContext.Provider value={state.sfuConfig}>
{children}
</SFUConfigContext.Provider>
);
}
}
type SFUConfigLoading = {
kind: "loading";
};
type SFUConfigLoaded = {
kind: "loaded";
sfuConfig: SFUConfig;
};
type SFUConfigFailed = {
kind: "failed";
error: Error;
};

54
src/livekit/openIDSFU.ts Normal file
View file

@ -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<SFUConfig> {
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();
}

View file

@ -1,8 +1,9 @@
import { Room, RoomOptions } from "livekit-client"; import { Room, RoomOptions } from "livekit-client";
import { useLiveKitRoom, useToken } from "@livekit/components-react"; import { useLiveKitRoom } from "@livekit/components-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { defaultLiveKitOptions } from "./options"; import { defaultLiveKitOptions } from "./options";
import { SFUConfig } from "./openIDSFU";
export type UserChoices = { export type UserChoices = {
audio?: DeviceChoices; audio?: DeviceChoices;
@ -14,29 +15,10 @@ export type DeviceChoices = {
enabled: boolean; enabled: boolean;
}; };
export type LiveKitConfig = {
sfuUrl: string;
jwtUrl: string;
roomName: string;
userDisplayName: string;
userIdentity: string;
};
export function useLiveKit( export function useLiveKit(
userChoices: UserChoices, userChoices: UserChoices,
config: LiveKitConfig sfuConfig: SFUConfig
): Room | undefined { ): 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 roomOptions = useMemo((): RoomOptions => {
const options = defaultLiveKitOptions; const options = defaultLiveKitOptions;
options.videoCaptureDefaults = { options.videoCaptureDefaults = {
@ -51,8 +33,8 @@ export function useLiveKit(
}, [userChoices.video, userChoices.audio]); }, [userChoices.video, userChoices.audio]);
const { room } = useLiveKitRoom({ const { room } = useLiveKitRoom({
token, token: sfuConfig.jwt,
serverUrl: config.sfuUrl, serverUrl: sfuConfig.url,
audio: userChoices.audio?.enabled ?? false, audio: userChoices.audio?.enabled ?? false,
video: userChoices.video?.enabled ?? false, video: userChoices.video?.enabled ?? false,
options: roomOptions, options: roomOptions,

View file

@ -28,13 +28,14 @@ import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView"; import { LobbyView } from "./LobbyView";
import { MatrixInfo } from "./VideoPreview"; import { MatrixInfo } from "./VideoPreview";
import { ActiveCall } from "./InCallView";
import { CallEndedView } from "./CallEndedView"; import { CallEndedView } from "./CallEndedView";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { UserChoices } from "../livekit/useLiveKit"; import { UserChoices } from "../livekit/useLiveKit";
import { findDeviceByName } from "../media-utils"; import { findDeviceByName } from "../media-utils";
import { OpenIDLoader } from "../livekit/OpenIDLoader";
import { ActiveCall } from "./InCallView";
declare global { declare global {
interface Window { interface Window {
@ -218,21 +219,39 @@ export function GroupCallView({
undefined undefined
); );
const [livekitServiceURL, setLivekitServiceURL] = useState<
string | undefined
>(groupCall.foci[0]?.livekitServiceUrl);
useEffect(() => {
setLivekitServiceURL(groupCall.foci[0]?.livekitServiceUrl);
}, [setLivekitServiceURL, groupCall]);
if (!livekitServiceURL) {
return <ErrorView error={new Error("No livekit_service_url defined")} />;
}
if (error) { if (error) {
return <ErrorView error={error} />; return <ErrorView error={error} />;
} else if (state === GroupCallState.Entered && userChoices) { } else if (state === GroupCallState.Entered && userChoices) {
return ( return (
<ActiveCall <OpenIDLoader
groupCall={groupCall}
client={client} client={client}
participants={participants} livekitServiceURL={livekitServiceURL}
onLeave={onLeave} roomName={matrixInfo.roomName}
unencryptedEventsFromUsers={unencryptedEventsFromUsers} >
hideHeader={hideHeader} <ActiveCall
matrixInfo={matrixInfo} client={client}
userChoices={userChoices} groupCall={groupCall}
otelGroupCallMembership={otelGroupCallMembership} participants={participants}
/> onLeave={onLeave}
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
hideHeader={hideHeader}
matrixInfo={matrixInfo}
userChoices={userChoices}
otelGroupCallMembership={otelGroupCallMembership}
/>
</OpenIDLoader>
); );
} else if (left) { } else if (left) {
// The call ended view is shown for two reasons: prompting guests to create // The call ended view is shown for two reasons: prompting guests to create

View file

@ -83,6 +83,7 @@ import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
import { useMediaDevices } from "../livekit/useMediaDevices"; import { useMediaDevices } from "../livekit/useMediaDevices";
import { useFullscreen } from "./useFullscreen"; import { useFullscreen } from "./useFullscreen";
import { useLayoutStates } from "../video-grid/Layout"; import { useLayoutStates } from "../video-grid/Layout";
import { useSFUConfig } from "../livekit/OpenIDLoader";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// There is currently a bug in Safari our our code with cloning and sending MediaStreams // 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. // For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
interface ActiveCallProps extends Omit<Props, "livekitRoom"> { export interface ActiveCallProps extends Omit<InCallViewProps, "livekitRoom"> {
userChoices: UserChoices; userChoices: UserChoices;
} }
export function ActiveCall(props: ActiveCallProps) { export function ActiveCall(props: ActiveCallProps) {
const livekitRoom = useLiveKit(props.userChoices, { const sfuConfig = useSFUConfig();
sfuUrl: props.groupCall.foci[0]!.url, const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
jwtUrl: `${props.groupCall.foci[0]!.jwtServiceUrl}/token`,
roomName: props.matrixInfo.roomName,
userDisplayName: props.matrixInfo.displayName,
userIdentity: `${props.client.getUserId()}:${props.client.getDeviceId()}`,
});
return ( return (
livekitRoom && ( livekitRoom && (
@ -112,7 +108,7 @@ export function ActiveCall(props: ActiveCallProps) {
); );
} }
interface Props { export interface InCallViewProps {
client: MatrixClient; client: MatrixClient;
groupCall: GroupCall; groupCall: GroupCall;
livekitRoom: Room; livekitRoom: Room;
@ -134,7 +130,7 @@ export function InCallView({
hideHeader, hideHeader,
matrixInfo, matrixInfo,
otelGroupCallMembership, otelGroupCallMembership,
}: Props) { }: InCallViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
usePreventScroll(); usePreventScroll();