Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2071,6 +2071,42 @@ const docTemplate = `{
}
}
},
"config.APIAuthConfig": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/config.APIUser"
}
}
}
},
"config.APIUser": {
"type": "object",
"properties": {
"password_hash": {
"type": "string"
},
"role": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"config.AuthConfig": {
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/config.APIAuthConfig"
}
}
},
"config.BufferConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -2442,6 +2478,8 @@ const docTemplate = `{
"domain.EventType": {
"type": "string",
"enum": [
"session.opened",
"session.closed",
"stream.created",
"stream.updated",
"stream.started",
Expand Down Expand Up @@ -2475,9 +2513,7 @@ const docTemplate = `{
"template.updated",
"template.deleted",
"stream.runtime_created",
"stream.runtime_expired",
"session.opened",
"session.closed"
"stream.runtime_expired"
],
"x-enum-comments": {
"EventDVRSegmentPruned": "retention loop deleted an aged-out segment",
Expand All @@ -2494,6 +2530,8 @@ const docTemplate = `{
"EventStreamUpdated": "PUT /streams/{code} on existing record"
},
"x-enum-descriptions": [
"",
"",
"",
"PUT /streams/{code} on existing record",
"",
Expand Down Expand Up @@ -2527,11 +2565,11 @@ const docTemplate = `{
"",
"",
"",
"",
"",
""
],
"x-enum-varnames": [
"EventSessionOpened",
"EventSessionClosed",
"EventStreamCreated",
"EventStreamUpdated",
"EventStreamStarted",
Expand Down Expand Up @@ -2565,14 +2603,15 @@ const docTemplate = `{
"EventTemplateUpdated",
"EventTemplateDeleted",
"EventStreamRuntimeCreated",
"EventStreamRuntimeExpired",
"EventSessionOpened",
"EventSessionClosed"
"EventStreamRuntimeExpired"
]
},
"domain.GlobalConfig": {
"type": "object",
"properties": {
"auth": {
"$ref": "#/definitions/config.AuthConfig"
},
"buffer": {
"$ref": "#/definitions/config.BufferConfig"
},
Expand Down
55 changes: 47 additions & 8 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -2064,6 +2064,42 @@
}
}
},
"config.APIAuthConfig": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"users": {
"type": "array",
"items": {
"$ref": "#/definitions/config.APIUser"
}
}
}
},
"config.APIUser": {
"type": "object",
"properties": {
"password_hash": {
"type": "string"
},
"role": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"config.AuthConfig": {
"type": "object",
"properties": {
"api": {
"$ref": "#/definitions/config.APIAuthConfig"
}
}
},
"config.BufferConfig": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -2435,6 +2471,8 @@
"domain.EventType": {
"type": "string",
"enum": [
"session.opened",
"session.closed",
"stream.created",
"stream.updated",
"stream.started",
Expand Down Expand Up @@ -2468,9 +2506,7 @@
"template.updated",
"template.deleted",
"stream.runtime_created",
"stream.runtime_expired",
"session.opened",
"session.closed"
"stream.runtime_expired"
],
"x-enum-comments": {
"EventDVRSegmentPruned": "retention loop deleted an aged-out segment",
Expand All @@ -2487,6 +2523,8 @@
"EventStreamUpdated": "PUT /streams/{code} on existing record"
},
"x-enum-descriptions": [
"",
"",
"",
"PUT /streams/{code} on existing record",
"",
Expand Down Expand Up @@ -2520,11 +2558,11 @@
"",
"",
"",
"",
"",
""
],
"x-enum-varnames": [
"EventSessionOpened",
"EventSessionClosed",
"EventStreamCreated",
"EventStreamUpdated",
"EventStreamStarted",
Expand Down Expand Up @@ -2558,14 +2596,15 @@
"EventTemplateUpdated",
"EventTemplateDeleted",
"EventStreamRuntimeCreated",
"EventStreamRuntimeExpired",
"EventSessionOpened",
"EventSessionClosed"
"EventStreamRuntimeExpired"
]
},
"domain.GlobalConfig": {
"type": "object",
"properties": {
"auth": {
"$ref": "#/definitions/config.AuthConfig"
},
"buffer": {
"$ref": "#/definitions/config.BufferConfig"
},
Expand Down
37 changes: 31 additions & 6 deletions api/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,29 @@ definitions:
total:
type: integer
type: object
config.APIAuthConfig:
properties:
enabled:
type: boolean
users:
items:
$ref: '#/definitions/config.APIUser'
type: array
type: object
config.APIUser:
properties:
password_hash:
type: string
role:
type: string
username:
type: string
type: object
config.AuthConfig:
properties:
api:
$ref: '#/definitions/config.APIAuthConfig'
type: object
config.BufferConfig:
properties:
capacity:
Expand Down Expand Up @@ -639,6 +662,8 @@ definitions:
type: object
domain.EventType:
enum:
- session.opened
- session.closed
- stream.created
- stream.updated
- stream.started
Expand Down Expand Up @@ -673,8 +698,6 @@ definitions:
- template.deleted
- stream.runtime_created
- stream.runtime_expired
- session.opened
- session.closed
type: string
x-enum-comments:
EventDVRSegmentPruned: retention loop deleted an aged-out segment
Expand All @@ -692,6 +715,8 @@ definitions:
EventStreamUpdated: PUT /streams/{code} on existing record
x-enum-descriptions:
- ""
- ""
- ""
- PUT /streams/{code} on existing record
- ""
- ""
Expand Down Expand Up @@ -725,9 +750,9 @@ definitions:
- ""
- ""
- ""
- ""
- ""
x-enum-varnames:
- EventSessionOpened
- EventSessionClosed
- EventStreamCreated
- EventStreamUpdated
- EventStreamStarted
Expand Down Expand Up @@ -762,10 +787,10 @@ definitions:
- EventTemplateDeleted
- EventStreamRuntimeCreated
- EventStreamRuntimeExpired
- EventSessionOpened
- EventSessionClosed
domain.GlobalConfig:
properties:
auth:
$ref: '#/definitions/config.AuthConfig'
buffer:
$ref: '#/definitions/config.BufferConfig'
hooks:
Expand Down
38 changes: 38 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package main

import (
"bufio"
"context"
"errors"
"fmt"
Expand All @@ -15,6 +16,8 @@ import (
"syscall"
"time"

"golang.org/x/crypto/bcrypt"

"github.com/ntt0601zcoder/open-streamer/config"
"github.com/ntt0601zcoder/open-streamer/internal/api"
"github.com/ntt0601zcoder/open-streamer/internal/api/handler"
Expand Down Expand Up @@ -42,12 +45,46 @@ import (
)

func main() {
// `open-streamer hashpw [password]` prints a bcrypt hash for an API user's
// password_hash field, so operators never put plaintext in the config.
if len(os.Args) > 1 && os.Args[1] == "hashpw" {
if err := runHashPw(os.Args[2:]); err != nil {
fmt.Fprintln(os.Stderr, "hashpw:", err)
os.Exit(1)
}
return
}
if err := run(); err != nil {
slog.Error("server: fatal error", "err", err)
os.Exit(1)
}
}

// runHashPw reads a password from the first arg, or from stdin when no arg is
// given (so it stays out of shell history), and prints its bcrypt hash.
func runHashPw(args []string) error {
var pw string
if len(args) > 0 {
pw = args[0]
} else {
fmt.Fprint(os.Stderr, "password: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil && line == "" {
return fmt.Errorf("read password: %w", err)
}
pw = strings.TrimRight(line, "\r\n")
}
if pw == "" {
return errors.New("empty password")
}
h, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("hash: %w", err)
}
fmt.Println(string(h))
return nil
}

func run() error {
// 1. Load StorageConfig from file/env — only storage settings come from viper.
storageCfg, err := config.LoadStorage()
Expand Down Expand Up @@ -192,6 +229,7 @@ func provideSubConfigs(i *do.RootScope, gcfg *domain.GlobalConfig) {
do.ProvideValue(i, deref(gcfg.Sessions))
do.ProvideValue(i, deref(gcfg.Watermarks))
do.ProvideValue(i, deref(gcfg.Log))
do.ProvideValue(i, deref(gcfg.Auth))
}

func deref[T any](p *T) T {
Expand Down
27 changes: 27 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,33 @@ type HooksConfig struct {
BatchMaxQueueItems int `mapstructure:"batch_max_queue_items" json:"batch_max_queue_items,omitempty" yaml:"batch_max_queue_items,omitempty"`
}

// AuthConfig holds authentication settings. Today it covers only the
// control-plane HTTP API (admin); media/playback auth is a separate plane.
type AuthConfig struct {
API APIAuthConfig `mapstructure:"api" json:"api" yaml:"api"`
}

// APIAuthConfig configures HTTP Basic auth on the management API. When
// Enabled is false (default) the API is open, preserving prior behaviour.
// Users are part of the global config (managed via the config API by a
// `write` user). On boot, if Enabled is true and Users is empty, an `admin`
// user with the `write` role is seeded from OPEN_STREAMER_API_ADMIN_PASSWORD
// when that env var is set — otherwise every request is rejected until users
// are defined (fail-closed).
type APIAuthConfig struct {
Enabled bool `mapstructure:"enabled" json:"enabled" yaml:"enabled"`
Users []APIUser `mapstructure:"users" json:"users,omitempty" yaml:"users,omitempty"`
}

// APIUser is one management-API account. Role is "read" (GET/HEAD/OPTIONS only)
// or "write" (all methods). PasswordHash is a bcrypt hash — never store a
// plaintext password here (generate one with `open-streamer hashpw`).
type APIUser struct {
Username string `mapstructure:"username" json:"username" yaml:"username"`
PasswordHash string `mapstructure:"password_hash" json:"password_hash" yaml:"password_hash"`
Role string `mapstructure:"role" json:"role" yaml:"role"`
}

// LogConfig controls structured logging output.
type LogConfig struct {
Level string `mapstructure:"level" json:"level" yaml:"level"` // trace | debug | info | warn | error
Expand Down
Loading