Rename the active product, Go module, binaries, configuration, protocols and documentation to Piik while retaining the TV mascot and existing media/room behavior. Use one current private contract without old-name aliases. Preserve SQLite authority and copy compatible local settings once; other settings start fresh. Git-message cleanup and the infrastructure name cutover are coordinated with this phase, with original history and deployment data archived externally. Validated locally with Web and Go checks, Windows capture compilation, matched Client/Server builds, packaged Client control handshake and history-rewrite integrity checks. Skip duplicate hosted CI for this owner-authorized local release. [skip ci]
640 lines
18 KiB
Go
640 lines
18 KiB
Go
package room
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
// The pure-Go driver: the port must build with CGO_ENABLED=0 (D2).
|
|
_ "modernc.org/sqlite"
|
|
|
|
"github.com/TNTcraftHIM/Piik/internal/server/protocol"
|
|
)
|
|
|
|
// Database identity and the single current schema are checked before adopting
|
|
// room authority. The application ID is ASCII "SCRN".
|
|
const (
|
|
roomDatabaseApplicationID = 0x5343524e
|
|
roomDatabaseSchemaVersion = 2
|
|
)
|
|
|
|
// roomSchema stores durable authority only, never presence or media state.
|
|
const roomSchema = `
|
|
CREATE TABLE rooms (
|
|
room_id TEXT PRIMARY KEY NOT NULL
|
|
CHECK (room_id GLOB '[1-9][0-9][0-9][0-9]'),
|
|
host_token_digest BLOB NOT NULL
|
|
CHECK (typeof(host_token_digest) = 'blob' AND length(host_token_digest) = 32),
|
|
viewer_grant_digest BLOB NOT NULL
|
|
CHECK (typeof(viewer_grant_digest) = 'blob' AND length(viewer_grant_digest) = 32),
|
|
viewer_authorization_generation TEXT NOT NULL
|
|
CHECK (
|
|
length(viewer_authorization_generation) = 22 AND
|
|
viewer_authorization_generation NOT GLOB '*[^A-Za-z0-9_-]*'
|
|
),
|
|
code_entry_policy TEXT NOT NULL
|
|
CHECK (code_entry_policy IN ('open', 'private')),
|
|
viewer_password_material BLOB NULL
|
|
CHECK (
|
|
viewer_password_material IS NULL OR
|
|
(typeof(viewer_password_material) = 'blob' AND length(viewer_password_material) = 48)
|
|
)
|
|
) STRICT;
|
|
PRAGMA application_id = 1396920910;
|
|
PRAGMA user_version = 2;
|
|
`
|
|
|
|
const selectStoredRooms = `SELECT
|
|
room_id,
|
|
host_token_digest,
|
|
viewer_grant_digest,
|
|
viewer_authorization_generation,
|
|
code_entry_policy,
|
|
viewer_password_material
|
|
FROM rooms
|
|
ORDER BY room_id`
|
|
|
|
const insertStoredRoom = `INSERT INTO rooms (
|
|
room_id,
|
|
host_token_digest,
|
|
viewer_grant_digest,
|
|
viewer_authorization_generation,
|
|
code_entry_policy,
|
|
viewer_password_material
|
|
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
|
|
const deleteRoomStatement = "DELETE FROM rooms WHERE room_id = ? AND host_token_digest = ?"
|
|
|
|
// StoredRoomAuthority excludes transient sessions and presence.
|
|
type StoredRoomAuthority struct {
|
|
RoomID string
|
|
HostTokenDigest []byte
|
|
ViewerGrantDigest []byte
|
|
ViewerAuthorizationGeneration string
|
|
CodeEntryPolicy protocol.CodeEntryPolicy
|
|
ViewerPasswordMaterial []byte
|
|
}
|
|
|
|
// Database is RoomDatabase: the SQLite stable authority. It holds one
|
|
// connection for its lifetime, because `PRAGMA locking_mode = EXCLUSIVE` is a
|
|
// connection property that a recycled pool connection would silently drop (D2).
|
|
type Database struct {
|
|
path string
|
|
db *sql.DB
|
|
conn *sql.Conn
|
|
// inTransaction reproduces DatabaseSync.isTransaction, which the driver
|
|
// does not expose; the ROLLBACK guard depends on it.
|
|
inTransaction bool
|
|
}
|
|
|
|
// NewDatabase is the RoomDatabase constructor.
|
|
func NewDatabase(path string) (*Database, error) {
|
|
if path == "" || path == ":memory:" {
|
|
return nil, errors.New("Room database path must identify a file")
|
|
}
|
|
return &Database{path: path}, nil
|
|
}
|
|
|
|
// Initialize opens the file, takes the exclusive lock, creates or verifies the
|
|
// schema and restores all stored room authority.
|
|
func (d *Database) Initialize() (restored []StoredRoomAuthority, err error) {
|
|
if d.conn != nil {
|
|
return nil, errors.New("Room database is already initialized")
|
|
}
|
|
|
|
// `new DatabaseSync(this.path, { timeout: 0 })`: a lock conflict must fail
|
|
// immediately instead of waiting.
|
|
db, err := sql.Open("sqlite", "file:"+uriPath(d.path)+"?_pragma=busy_timeout(0)")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
db.SetConnMaxLifetime(0)
|
|
db.SetConnMaxIdleTime(0)
|
|
conn, err := db.Conn(context.Background())
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return nil, err
|
|
}
|
|
d.db, d.conn = db, conn
|
|
defer func() {
|
|
if err != nil {
|
|
d.closeHandles()
|
|
}
|
|
}()
|
|
|
|
// The pragma answers "exclusive" even when another process owns the file;
|
|
// the BEGIN EXCLUSIVE below is what actually takes the lock.
|
|
var lockingMode string
|
|
if err = d.conn.QueryRowContext(
|
|
context.Background(), "PRAGMA main.locking_mode = EXCLUSIVE").Scan(&lockingMode); err != nil {
|
|
return nil, err
|
|
}
|
|
if lockingMode != "exclusive" {
|
|
return nil, errors.New("Room database could not acquire exclusive locking mode")
|
|
}
|
|
if err = d.begin("BEGIN EXCLUSIVE"); err != nil {
|
|
return nil, err
|
|
}
|
|
restored, err = d.recover()
|
|
if err != nil {
|
|
d.rollbackIfNeeded()
|
|
return nil, err
|
|
}
|
|
if err = d.finish("COMMIT"); err != nil {
|
|
d.rollbackIfNeeded()
|
|
return nil, err
|
|
}
|
|
return restored, nil
|
|
}
|
|
|
|
// uriPath is the literal path `new DatabaseSync(this.path)` opened, written for
|
|
// the driver's URI DSN: the driver splits on the first `?` and SQLite decodes
|
|
// `%HH` and stops at `#`, so those three characters are escaped.
|
|
func uriPath(path string) string {
|
|
return strings.NewReplacer("%", "%25", "#", "%23", "?", "%3F").Replace(filepath.ToSlash(path))
|
|
}
|
|
|
|
func (d *Database) recover() ([]StoredRoomAuthority, error) {
|
|
if err := d.ensureExactSchema(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := d.assertDatabaseIntegrity(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d.readStoredRooms()
|
|
}
|
|
|
|
// InsertRoom is insertRoom.
|
|
func (d *Database) InsertRoom(room StoredRoomAuthority) error {
|
|
if err := assertStoredRoom(room); err != nil {
|
|
return err
|
|
}
|
|
return d.transaction(func() error {
|
|
changes, err := d.exec(insertStoredRoom,
|
|
room.RoomID,
|
|
room.HostTokenDigest,
|
|
room.ViewerGrantDigest,
|
|
room.ViewerAuthorizationGeneration,
|
|
room.CodeEntryPolicy,
|
|
room.ViewerPasswordMaterial)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(changes, "insert")
|
|
})
|
|
}
|
|
|
|
// SetViewerPassword is setViewerPassword; nil material clears the password.
|
|
func (d *Database) SetViewerPassword(
|
|
roomID string, hostTokenDigest, viewerPasswordMaterial []byte,
|
|
) error {
|
|
if err := assertRoomIdentity(roomID, hostTokenDigest); err != nil {
|
|
return err
|
|
}
|
|
if err := assertPasswordMaterial(viewerPasswordMaterial); err != nil {
|
|
return err
|
|
}
|
|
return d.transaction(func() error {
|
|
changes, err := d.exec(
|
|
`UPDATE rooms
|
|
SET viewer_password_material = ?
|
|
WHERE room_id = ? AND host_token_digest = ?`,
|
|
viewerPasswordMaterial, roomID, hostTokenDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(changes, "password update")
|
|
})
|
|
}
|
|
|
|
// SetCodeEntryPolicy is setCodeEntryPolicy.
|
|
func (d *Database) SetCodeEntryPolicy(
|
|
roomID string, hostTokenDigest []byte, codeEntryPolicy protocol.CodeEntryPolicy,
|
|
) error {
|
|
if err := assertRoomIdentity(roomID, hostTokenDigest); err != nil {
|
|
return err
|
|
}
|
|
if !validCodeEntryPolicy(codeEntryPolicy) {
|
|
return errors.New("Room code-entry policy is invalid")
|
|
}
|
|
return d.transaction(func() error {
|
|
changes, err := d.exec(
|
|
`UPDATE rooms
|
|
SET code_entry_policy = ?
|
|
WHERE room_id = ? AND host_token_digest = ?`,
|
|
codeEntryPolicy, roomID, hostTokenDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(changes, "code-entry policy update")
|
|
})
|
|
}
|
|
|
|
// SetViewerGrant is setViewerGrant.
|
|
func (d *Database) SetViewerGrant(
|
|
roomID string, hostTokenDigest, viewerGrantDigest []byte,
|
|
viewerAuthorizationGeneration string,
|
|
) error {
|
|
if err := assertRoomIdentity(roomID, hostTokenDigest); err != nil {
|
|
return err
|
|
}
|
|
if err := assertDigest(viewerGrantDigest, "Viewer grant"); err != nil {
|
|
return err
|
|
}
|
|
if err := assertAuthorizationGeneration(viewerAuthorizationGeneration); err != nil {
|
|
return err
|
|
}
|
|
return d.transaction(func() error {
|
|
changes, err := d.exec(
|
|
`UPDATE rooms
|
|
SET viewer_grant_digest = ?,
|
|
viewer_authorization_generation = ?
|
|
WHERE room_id = ? AND host_token_digest = ?`,
|
|
viewerGrantDigest, viewerAuthorizationGeneration, roomID, hostTokenDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(changes, "Viewer grant update")
|
|
})
|
|
}
|
|
|
|
// DeleteRoom is deleteRoom.
|
|
func (d *Database) DeleteRoom(roomID string, hostTokenDigest []byte) error {
|
|
if err := assertRoomIdentity(roomID, hostTokenDigest); err != nil {
|
|
return err
|
|
}
|
|
return d.transaction(func() error {
|
|
changes, err := d.exec(deleteRoomStatement, roomID, hostTokenDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(changes, "delete")
|
|
})
|
|
}
|
|
|
|
// ReplaceRoom is replaceRoom: the new room and the removal of the old one are
|
|
// one authority transition.
|
|
func (d *Database) ReplaceRoom(
|
|
oldRoomID string, oldHostTokenDigest []byte, replacement StoredRoomAuthority,
|
|
) error {
|
|
if err := assertRoomIdentity(oldRoomID, oldHostTokenDigest); err != nil {
|
|
return err
|
|
}
|
|
if err := assertStoredRoom(replacement); err != nil {
|
|
return err
|
|
}
|
|
if replacement.RoomID == oldRoomID {
|
|
return errors.New("Replacement room ID must be different")
|
|
}
|
|
return d.transaction(func() error {
|
|
inserted, err := d.exec(insertStoredRoom,
|
|
replacement.RoomID,
|
|
replacement.HostTokenDigest,
|
|
replacement.ViewerGrantDigest,
|
|
replacement.ViewerAuthorizationGeneration,
|
|
replacement.CodeEntryPolicy,
|
|
replacement.ViewerPasswordMaterial)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := assertSingleChange(inserted, "replacement insert"); err != nil {
|
|
return err
|
|
}
|
|
deleted, err := d.exec(deleteRoomStatement, oldRoomID, oldHostTokenDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return assertSingleChange(deleted, "replacement delete")
|
|
})
|
|
}
|
|
|
|
// Close closes the connection; it is safe on a database that never opened one.
|
|
func (d *Database) Close() error {
|
|
return d.closeHandles()
|
|
}
|
|
|
|
func (d *Database) closeHandles() error {
|
|
conn, db := d.conn, d.db
|
|
d.conn, d.db, d.inTransaction = nil, nil, false
|
|
var connError error
|
|
if conn != nil {
|
|
connError = conn.Close()
|
|
}
|
|
if db != nil {
|
|
if err := db.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return connError
|
|
}
|
|
|
|
func (d *Database) ensureExactSchema() error {
|
|
applicationID, err := d.readPragmaInteger("application_id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
schemaVersion, err := d.readPragmaInteger("user_version")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
schemaObjects, err := d.readSchemaObjects()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if applicationID == 0 && schemaVersion == 0 && len(schemaObjects) == 0 {
|
|
_, err := d.conn.ExecContext(context.Background(), roomSchema)
|
|
return err
|
|
}
|
|
|
|
if applicationID != roomDatabaseApplicationID {
|
|
return errors.New("Room database application identity does not match")
|
|
}
|
|
if schemaVersion != roomDatabaseSchemaVersion {
|
|
return errors.New("Room database schema version does not match")
|
|
}
|
|
if len(schemaObjects) != 1 ||
|
|
schemaObjects[0][0] != "table" || schemaObjects[0][1] != "rooms" {
|
|
return errors.New("Room database schema objects do not match")
|
|
}
|
|
return d.assertExactColumns()
|
|
}
|
|
|
|
func (d *Database) readSchemaObjects() ([][2]string, error) {
|
|
rows, err := d.conn.QueryContext(context.Background(),
|
|
`SELECT type, name
|
|
FROM sqlite_schema
|
|
WHERE name NOT LIKE 'sqlite_%'
|
|
ORDER BY type, name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var objects [][2]string
|
|
for rows.Next() {
|
|
var object [2]string
|
|
if err := rows.Scan(&object[0], &object[1]); err != nil {
|
|
return nil, err
|
|
}
|
|
objects = append(objects, object)
|
|
}
|
|
return objects, rows.Err()
|
|
}
|
|
|
|
func (d *Database) assertDatabaseIntegrity() error {
|
|
rows, err := d.conn.QueryContext(context.Background(), "PRAGMA quick_check")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
var results []string
|
|
for rows.Next() {
|
|
var result string
|
|
if err := rows.Scan(&result); err != nil {
|
|
return err
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if len(results) != 1 || results[0] != "ok" {
|
|
return errors.New("Room database integrity check failed")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) assertExactColumns() error {
|
|
expected := [...]struct {
|
|
name string
|
|
columnType string
|
|
notNull int
|
|
primaryKey int
|
|
}{
|
|
{"room_id", "TEXT", 1, 1},
|
|
{"host_token_digest", "BLOB", 1, 0},
|
|
{"viewer_grant_digest", "BLOB", 1, 0},
|
|
{"viewer_authorization_generation", "TEXT", 1, 0},
|
|
{"code_entry_policy", "TEXT", 1, 0},
|
|
{"viewer_password_material", "BLOB", 0, 0},
|
|
}
|
|
rows, err := d.conn.QueryContext(context.Background(), "PRAGMA table_info(rooms)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
index := 0
|
|
for rows.Next() {
|
|
var (
|
|
cid int
|
|
name string
|
|
columnType string
|
|
notNull int
|
|
defaultValue sql.NullString
|
|
primaryKey int
|
|
)
|
|
if err := rows.Scan(
|
|
&cid, &name, &columnType, ¬Null, &defaultValue, &primaryKey); err != nil {
|
|
return err
|
|
}
|
|
if index >= len(expected) || name != expected[index].name ||
|
|
columnType != expected[index].columnType ||
|
|
notNull != expected[index].notNull ||
|
|
primaryKey != expected[index].primaryKey {
|
|
return errors.New("Room database columns do not match the current schema")
|
|
}
|
|
index++
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if index != len(expected) {
|
|
return errors.New("Room database columns do not match the current schema")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) readStoredRooms() ([]StoredRoomAuthority, error) {
|
|
rows, err := d.conn.QueryContext(context.Background(), selectStoredRooms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var stored []StoredRoomAuthority
|
|
for rows.Next() {
|
|
var (
|
|
room StoredRoomAuthority
|
|
policy string
|
|
material []byte
|
|
)
|
|
if err := rows.Scan(
|
|
&room.RoomID,
|
|
&room.HostTokenDigest,
|
|
&room.ViewerGrantDigest,
|
|
&room.ViewerAuthorizationGeneration,
|
|
&policy,
|
|
&material); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := readDigest(room.HostTokenDigest, "Host token"); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := readDigest(room.ViewerGrantDigest, "Viewer grant"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validCodeEntryPolicy(policy) {
|
|
return nil, errors.New("Room database contains an invalid code-entry policy")
|
|
}
|
|
room.CodeEntryPolicy = policy
|
|
if material != nil {
|
|
if len(material) != viewerPasswordMaterialBytes {
|
|
return nil, errors.New("Room database contains invalid Viewer password material")
|
|
}
|
|
room.ViewerPasswordMaterial = material
|
|
}
|
|
if err := assertStoredRoom(room); err != nil {
|
|
return nil, err
|
|
}
|
|
stored = append(stored, room)
|
|
}
|
|
return stored, rows.Err()
|
|
}
|
|
|
|
func (d *Database) readPragmaInteger(name string) (int64, error) {
|
|
var value int64
|
|
if err := d.conn.QueryRowContext(
|
|
context.Background(), "PRAGMA "+name).Scan(&value); err != nil {
|
|
return 0, fmt.Errorf("Room database %s is invalid", name)
|
|
}
|
|
if value < -protocol.MaxSafeInteger || value > protocol.MaxSafeInteger {
|
|
return 0, fmt.Errorf("Room database %s is invalid", name)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (d *Database) transaction(operation func() error) error {
|
|
if d.conn == nil {
|
|
return errors.New("Room database is not initialized")
|
|
}
|
|
if err := d.begin("BEGIN IMMEDIATE"); err != nil {
|
|
return err
|
|
}
|
|
if err := operation(); err != nil {
|
|
d.rollbackIfNeeded()
|
|
return err
|
|
}
|
|
if err := d.finish("COMMIT"); err != nil {
|
|
d.rollbackIfNeeded()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) begin(statement string) error {
|
|
if _, err := d.conn.ExecContext(context.Background(), statement); err != nil {
|
|
return err
|
|
}
|
|
d.inTransaction = true
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) finish(statement string) error {
|
|
if _, err := d.conn.ExecContext(context.Background(), statement); err != nil {
|
|
return err
|
|
}
|
|
d.inTransaction = false
|
|
return nil
|
|
}
|
|
|
|
// rollbackIfNeeded is `if (database.isTransaction) database.exec("ROLLBACK")`:
|
|
// a ROLLBACK with no active transaction is itself an error.
|
|
func (d *Database) rollbackIfNeeded() {
|
|
if d.inTransaction {
|
|
_ = d.finish("ROLLBACK")
|
|
}
|
|
}
|
|
|
|
func (d *Database) exec(query string, args ...any) (int64, error) {
|
|
result, err := d.conn.ExecContext(context.Background(), query, args...)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
func assertStoredRoom(room StoredRoomAuthority) error {
|
|
if !protocol.ValidRoomCode(room.RoomID) {
|
|
return errors.New("Room database contains an invalid room ID")
|
|
}
|
|
if err := assertDigest(room.HostTokenDigest, "Host token"); err != nil {
|
|
return err
|
|
}
|
|
if err := assertDigest(room.ViewerGrantDigest, "Viewer grant"); err != nil {
|
|
return err
|
|
}
|
|
if err := assertAuthorizationGeneration(room.ViewerAuthorizationGeneration); err != nil {
|
|
return err
|
|
}
|
|
if !validCodeEntryPolicy(room.CodeEntryPolicy) {
|
|
return errors.New("Room database contains an invalid code-entry policy")
|
|
}
|
|
if err := assertPasswordMaterial(room.ViewerPasswordMaterial); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertRoomIdentity(roomID string, hostTokenDigest []byte) error {
|
|
if !protocol.ValidRoomCode(roomID) {
|
|
return errors.New("Room ID is invalid")
|
|
}
|
|
return assertDigest(hostTokenDigest, "Host token")
|
|
}
|
|
|
|
func readDigest(value []byte, name string) error {
|
|
if len(value) != 32 {
|
|
return fmt.Errorf("Room database contains an invalid %s digest", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertDigest(value []byte, name string) error {
|
|
if len(value) != 32 {
|
|
return fmt.Errorf("%s digest must contain 32 bytes", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertPasswordMaterial(value []byte) error {
|
|
if value != nil && len(value) != viewerPasswordMaterialBytes {
|
|
return errors.New("Viewer password material must contain 48 bytes")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertAuthorizationGeneration(value string) error {
|
|
decoded, err := base64.RawURLEncoding.DecodeString(value)
|
|
if err != nil || len(decoded) != 16 ||
|
|
base64.RawURLEncoding.EncodeToString(decoded) != value {
|
|
return errors.New("Viewer authorization generation is invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertSingleChange(changes int64, operation string) error {
|
|
if changes != 1 {
|
|
return fmt.Errorf("Room database %s did not match current authority", operation)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validCodeEntryPolicy(policy protocol.CodeEntryPolicy) bool {
|
|
return policy == protocol.CodeEntryOpen || policy == protocol.CodeEntryPrivate
|
|
}
|