Open10

Pusher

RasukarusanRasukarusan

どうやらチャンネルにはライフタイムがあるらしい。30秒?

RasukarusanRasukarusan

確かにちゃんと30秒でdisconnectedになった。state_changeで観測可能。

RasukarusanRasukarusan

公式ドキュメントには秒数は書いていないが、タイムアウトされる旨は記載されていた
https://pusher.com/docs/channels/using_channels/authorized-connections/

If you have this feature enabled and a connection does not subscribe to at least one private- or presence- channel, it will be kicked off after a timeout.

以下で見つけた
https://stackoverflow.com/questions/73421901/laravel-echo-with-pusher-disconnect-after-30-seconds

Pusher ダッシュボードのチャンネル アプリ設定で、誤って有効にしていませんかEnable authorized connections? これを有効にすると、30 秒以内にプライベート チャネルへのアクセスが許可されない場合、クライアントは切断されます。

RasukarusanRasukarusan

チャンネルの暗号化

https://pusher.com/docs/channels/using_channels/authorized-connections/

  1. 悪意のあるユーザーに同時接続数を埋められないようにするため
  2. チャンネルに誰が接続しているのかを把握するため
  • private-
  • presence-

presenceはprivateの拡張版で、ユーザーを「登録」することで誰が今接続しているのかを把握できるようになる。

公開APP_KEYとチャンネル名を知っていれば誰でもチャンネルをサブスクライブできる。なので例えば他のアプリでpusherを初期化して接続することも可能なはず。
そうなるとクラッカーに同時接続数を消費されるおそれがある。
privateでチャンネル作成すれば、signinしたユーザーだけが同時接続数にカウントされる?

RasukarusanRasukarusan

private、presenceチャンネルの認証方法

import Pusher from 'pusher-js'
const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY, {
  cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER,
  userAuthentication: {
    endpoint: '/api/pusher/user-auth',
    transport: 'ajax',
    params: { hoge: 'fofo' },
    headers: { cussus: 'kasutamudayo' },
    paramsProvider: null,
    headersProvider: null,
  },
  channelAuthorization: {
    endpoint: '/api/pusher/auth',
    transport: 'ajax',
    params: { hoge: 'channel-fofo' },
    headers: { cussus: 'channel-kasutamudayo' },
    paramsProvider: null,
    headersProvider: null,
  },
})
export default pusher

ユーザー認証とチャンネル認証のそれぞれの設定がある。
presenceチャンネルの場合はチャンネル認証のみ(/api/pusher/auth)、privateチャンネルの場合はユーザー認証のみ(/api/pusher/user-auth)が走る

src/pages/api/pusher/auth.ts

import type { NextApiRequest, NextApiResponse } from 'next'
import { getServerSession } from 'next-auth'
import { authOptions } from '../auth/[...nextauth]'
import pusher from '@/libs/pusher/server'
import { auth } from '@/libs/pusher/util'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getServerSession(req, res, authOptions)
  if (!session) {
    res.status(401).json({ result: false })
  }
  const { socket_id, channel_name } = req.body
  const userId = session.user.id
  const isAuth = auth(userId, channel_name)
  if (isAuth) {
    const presenceData = {
      user_id: userId,
      user_info: { name: session.user.name, image: session.user.image },
    }
    const authResponse = pusher.authorizeChannel(
      socket_id,
      channel_name,
      presenceData
    )
    return res.send(authResponse)
  }
  return res.status(403).json({ result: false })
}

RasukarusanRasukarusan

ユーザー認証の場合はクライアント側でpusher.signin()をすると/pusher/user-authにリクエストが飛ぶ。んでpresenceDataに{id: string, user_info: object}を加えてauthResponseを返せば認証完了。
認証したユーザーにメッセージを送りたい場合は、クライアント側でpusher.user.bindをしておけばそこで受け取れる。なのでユーザー認証の場合はチャンネルいらん。

RasukarusanRasukarusan

複数タブを開いてpusher-jsでnew Pusherをして、pusher.allChannels()をしても同一タブで作成したチャンネルしか表示されない。しかし、同名のpresence-チャンネルを作成し参加し、メンバー情報を見てみると他のタブでも情報は共有される。
当たり前だが、pusherプラットフォーム上で管理されているからである。
別タブでも1つのpusher(pusherプラットフォーム上のグローバルなpusher)を参照したいときはある。シンプルにチャットルーム機能を実装しようとしたら欲しくなる。

サーバー側で下記のようにグローバルpusherの状態を取得できる。

  const c = await pusher
    .get({
      path: '/channels',
      params: { info: 'user_count', filter_by_prefix: 'presence-' },
    })
    .then((res) => res.json())
  console.log(c)

/*
{
  channels: {
    'presence-1': { user_count: 1 },
    'presence-12': { user_count: 1 },
    'presence-12k': { user_count: 2 },
    'presence-12kj': { user_count: 1 },
    'presence-12kjd': { user_count: 1 },
    'presence-12kjdfff': { user_count: 1 },
    'presence-clmhu3oxp000aa6qzy1mzcl50': { user_count: 1 },
    'presence-fd': { user_count: 1 },
    'presence-fdffff': { user_count: 1 },
    'presence-hoge': { user_count: 1 },
    'presence-hoge2': { user_count: 1 }
  }
}
*/
RasukarusanRasukarusan

タブを閉じても、そのタブで作ったチャンネルは削除されない。
ただしチャンネルに参加している人数が1人だけ(そのタブの人のみ)だった場合、チャンネルは削除される。