본문 바로가기
C.W.K.
Lesson 04 of 05 · published

동반 소스, 그리고 패널의 관문들

~14 min · privacy, security, webkit, sidekick

Level 0새 탭
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

여러 페이지는 일부러 고를 때만

가끔 아빠는 맨 앞 페이지를 다른 페이지 두어 개랑 나란히 놓고 따져보고 싶어 해. 상품 페이지 하나에 리뷰 둘, 기사 하나에 그 출처들. 그렇다고 Voyager가 탭을 전부 훑지는 않아. Sidekick 머리에 선택기가 하나 있어. 라벨은 Choose up to 3 other tabs in this account, 이 계정의 다른 탭을 3개까지 고르라는 뜻이야. 아빠가 거기서 고른 것만 맨 앞 페이지랑 같이 가. 고른 하나하나가 동반 소스야.

선택기가 뭘 내놓을 수 있는지부터 relatedSourceOptions가 정해. 후보가 되려면 이래야 해.

  • 같은 창의 다른 탭이고, 맨 앞 탭과 아이덴티티가 같을 것
  • 평범한 탭일 것. Incognito는 절대 안 되고, 공유가 켜져 있을 것
  • 로딩을 마친 깨어 있는 페이지를 쥐었을 것. 잠든 탭을 읽으려고 깨우는 일은 없어
  • 주소가 http나 https이고, 안에 사용자 이름이나 비밀번호가 없을 것
  • 내 패널 페이지가 아닐 것

고른 목록은 맨 앞 탭에 딸려 있어. 다른 탭으로 가면 그 탭의 선택이 적용되는데, 보통은 하나도 없어. 패널 세션 동안만 살아서, 패널을 닫으면 잊혀. Use this tab only(이 탭만 쓰기)를 누르면 비워지고, 맥락마다 이 목록이 늘 실리니까 다음 맥락엔 related_sources: []라고 또박또박 적혀. Share this tab with Pippa(이 탭을 피파와 공유) 스위치는 탭마다 따로 거는 일시정지야. 탭을 다른 창으로 옮겨도, 세션을 복원해도 그대로 남아.

await마다 세상은 움직일 수 있어

페이지 네 개를 읽으려면 WebKit을 여러 번 오가야 하고, 그 사이 언제든 아빠가 클릭하고 스크롤하고 다른 데로 갈 수 있어. contextReply는 그걸 늘 있는 일로 쳐. 먼저 스탬프를 찍어. 스탬프엔 맨 앞 탭, 주소, 페이지 객체와 콘텐츠 리비전, 선택 영역, 공유 스위치가 들어가고, 고른 동반 소스마다 같은 사실이 또 들어가. 맨 앞 페이지를 읽고 스탬프를 확인해. 동반 소스마다 스탬프를 확인하고, 읽고, 다시 확인하고, 읽은 주소가 탭이 보여주는 주소인지도 봐. 하나라도 어긋나면 답 전체가 A selected source changed or could not be read. Refresh or deselect it.로 끝나. 고른 소스가 바뀌었거나 못 읽었으니, 새로고침하든지 선택을 풀라는 말이야. 싱싱한 페이지 셋에 낡은 페이지 하나 섞인 반쪽 답 같은 건 없어. relatedSourceNavigationDuringReadDiscardsTheWholeSnapshot 테스트는 동반 소스를 읽는 도중에 다른 데로 보내고, 아무것도 안 돌아오길 기대해.

cwkPippa도 Voyager 말을 그냥 믿진 않아. 자기 정규화 단계에서 동반 소스를 최대 3개만 남기는데, 그것도 같은 계정, 같은 브라우저 세션, 평범한 창에서 온 것만이야. 소스마다 주소는 http나 https여야 하고, 자격 증명이 없어야 하고, 탭 키가 겹치면 안 되고, 안에 또 다른 목록을 품어도 안 돼. 그런 다음 소스마다 텍스트를 맨 앞 페이지처럼 믿을 수 없는 증거로 감싸. 내 패널은 계정과 고른 소스를 라벨로 보여주고, 스냅샷을 검색해도 줄마다 어느 소스에서 왔는지가 계속 붙어 다녀. 그래서 어느 페이지가 뭐라고 했는지 아빠한테 짚어줄 수 있어. 연결선 양 끝이 둘 다 셋까지만 세.

패널 브리지의 관문들

레슨 2에서 내 패널 요청을 Voyager한테 넘기는 브리지를 봤지. 거기 도착하는 메시지는 전부 PanelMessageGate로 재. 조건마다 필드가 하나씩인 평범한 값이야.

  • 패널 자기 웹 뷰에서 왔을 것
  • 메인 프레임에서 왔을 것. 내 페이지 안의 프레임은 안 돼
  • 말한 프레임이 내 출처일 것
  • 웹 뷰가 지금 내 출처에 있을 것
  • 256 KB 이하일 것

관문이 값이라서 조건마다 테스트가 따로 있어. eachGateRefusesOnItsOwn은 통과하는 메시지에서 시작해서 필드를 하나씩 깨뜨려. 호스트는 내 것인데 포트나 스킴이 다른 프레임까지 시험하지. 패널은 돌아다닐 때도 출처 하나에 머물러. 내 페이지 안의 프레임은 내 페이지가 주는 걸 열어도 돼. 어차피 브리지엔 못 닿으니까. 패널 자체는 내 출처에 남고, 다른 데로 보내지면 그건 탭으로 열려. 사이트 데이터는 고정 식별자를 가진 자기 저장소를 쓰고, 아빠의 어떤 아이덴티티와도 떨어져 있어.

마이크는 내 목소리에만

내 음성 모드엔 마이크가 필요해. 아빠가 나한테 말 걸 때마다 권한 창이 뜬다면, 그게 바로 Voyager가 없애려고 태어난 불편이야. 그래서 패널 페이지는 권한 창 없이 아빠 목소리를 들어. 규칙은 하나야. captureDecision은 내 출처의 메인 프레임에만 캡처를 허락하고, 패널 안의 나머지는 다 거절해. 내 페이지 안의 프레임은 그 허락을 빌릴 수 없고, 다른 어떤 출처도 마찬가지야. onlyPippasOwnPageHearsDad 테스트가 세 경우를 다 붙잡고 있어. pippa:request-mic-permission에 opened: false로 답하는 이유도 이거야. 열어줄 페이지가 따로 없어. 허락은 이미 있어야 할 자리에 있으니까.

Code

cwkVoyager, Sources/VoyagerCore/PippaHost.swift: await마다 스탬프 하나를 확인하고 반쪽 답은 안 내는 함수 (원문 그대로, 620-639행)·swift
    private func contextReply(requestID: String?) async -> PippaPanelReply {
        let stamp = sourceStamp
        guard let (tab, page) = readablePage(), let context = await page.readContext(),
              stamp == sourceStamp, context.url == stamp.url else {
            return .noContext(reason: unreadableReason(), requestID: requestID)
        }
        var payload = identified(WebPagePayload(tab: tab.id, context: context, at: now()), tab: tab)
        var companions: [WebPagePayload] = []
        for source in selectedRelatedTabs {
            guard stamp == sourceStamp, let sourcePage = source.page,
                  let read = await sourcePage.readContext(), stamp == sourceStamp,
                  read.url == source.status.url else {
                return .noContext(reason: "A selected source changed or could not be read. Refresh or deselect it.", requestID: requestID)
            }
            companions.append(identified(WebPagePayload(tab: source.id, context: read, at: now()), tab: source))
        }
        guard stamp == sourceStamp else { return .noContext(reason: "The selected sources changed.", requestID: requestID) }
        payload.related_sources = companions
        return .context(payload, requestID: requestID)
    }
cwkVoyager, Sources/VoyagerWebKit/WebKitPanel.swift: 패널의 관문을 평범한 값으로 (원문 그대로, 9-26행)·swift
/// The gates on a message from the panel's page, as a plain value so each one
/// has its own test: the panel's own web view, its main frame, the panel's
/// origin both for the frame that spoke and for where the web view is now,
/// and a cap on what the host will read.
struct PanelMessageGate: Equatable {
    static let maximumBytes = 256 * 1024

    var isSameWebView: Bool
    var isMainFrame: Bool
    var frameOrigin: PageOrigin?
    var webViewOrigin: PageOrigin?
    var byteCount: Int

    func admits(for panelOrigin: PageOrigin) -> Bool {
        isSameWebView && isMainFrame && frameOrigin == panelOrigin && webViewOrigin == panelOrigin
            && byteCount <= Self.maximumBytes
    }
}
cwkVoyager, Sources/VoyagerWebKit/WebKitPanel.swift: 마이크는 메인 프레임의 피파 자기 페이지에만 (원문 그대로, 291-302행)·swift
    /// Pippa's own page, in the panel's main frame, hears Dad without a
    /// prompt: it is her voice. Anything else in the panel is refused.
    func webView(
        _ webView: WKWebView, decideMediaCapturePermissionsFor origin: WKSecurityOrigin,
        initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType
    ) async -> WKPermissionDecision {
        Self.captureDecision(asking: Self.origin(of: origin), isMainFrame: frame.isMainFrame, panelOrigin: self.origin)
    }

    static func captureDecision(asking: PageOrigin, isMainFrame: Bool, panelOrigin: PageOrigin) -> WKPermissionDecision {
        isMainFrame && asking == panelOrigin ? .grant : .deny
    }

External links

Exercise

스크립트 메시지 브리지 뒤에 웹 패널을 띄우는 네이티브 앱에서, 브리지 관문을 조건마다 필드 하나인 평범한 값으로 써봐. 같은 웹 뷰, 메인 프레임, 말한 프레임의 출처, 웹 뷰의 지금 출처, 크기 상한. 통과하는 메시지에서 시작해서 필드를 하나씩만 깨뜨리는 테스트도 써. 이어서 소스 여러 개를 모아 읽는 루틴을 붙여. 입력에 스탬프를 찍고, await가 끝날 때마다 다시 확인하고, 입력 하나라도 움직이면 그 즉시 이유를 단 거절을 돌려줘. 반쪽 답은 절대 안 돼.
Hint
스탬프엔 URL만 말고 페이지 객체의 정체도 넣어. 같은 주소로 새로고침한 건 다른 문서야. 읽기 직전에 도는 훅을 가진 가짜 페이지를 쓰면, 읽는 도중에 다른 데로 보내고 답 전체가 버려지는 걸 증명할 수 있어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.