mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-10 18:02:17 +02:00
Web Ui Autrhentication
This commit is contained in:
+6
-1
@@ -14,7 +14,12 @@
|
||||
* Mapper can now set `epg_channel_id`.
|
||||
* Added environment variables for User Credentials `username`, `password` and `token` in format `${env:<EnvVarName>}` where `<EnvVarName>` should be replaced.
|
||||
* Added `web_ui_enabled` to `config.yml`. Default is `true`. Set to `false` to disable webui.
|
||||
* Added `web_auth_enabled` to `config.yml`. Default is `false`. Set to `true` to enable webui authentication.
|
||||
* Added `web_auth` to `config.yml` struct for web-ui-authentication is optional.
|
||||
- `enabled`: default true
|
||||
- `issuer` issuer for jwt token
|
||||
- `secret` secret for jwt token
|
||||
- `userfile` userfile with generated userfile in format "username: password" per file
|
||||
* Password generation argument --genpwd to generate passwords for userfile.
|
||||
|
||||
# v1.1.8(2024-03-06)
|
||||
* Fixed WebUI Option-Select
|
||||
|
||||
Generated
+23
@@ -1466,8 +1466,10 @@ dependencies = [
|
||||
"pest_derive",
|
||||
"petgraph",
|
||||
"quick-xml",
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rpassword",
|
||||
"rust-argon2",
|
||||
"rustelebot",
|
||||
"serde",
|
||||
@@ -2021,6 +2023,27 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "2.1.0"
|
||||
|
||||
@@ -47,3 +47,5 @@ bincode = "1.3"
|
||||
uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] }
|
||||
lzma-rs = "0.3"
|
||||
linereader = "0"
|
||||
rand = "0.8"
|
||||
rpassword = "7.3"
|
||||
|
||||
@@ -3,44 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="%PUBLIC_URL%/assets/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="%PUBLIC_URL%/assets/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="%PUBLIC_URL%/assets/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="%PUBLIC_URL%/assets/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/assets/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="%PUBLIC_URL%/assets/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="%PUBLIC_URL%/assets/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="%PUBLIC_URL%/assets/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/assets/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/assets/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/assets/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/assets/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/assets/favicon-16x16.png">
|
||||
<!-- link rel="manifest" href="/manifest.json" -->
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<!-- link rel="manifest" href="%PUBLIC_URL%/manifest.json" / -->
|
||||
<title>m3u-filter</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {Observable} from "rxjs";
|
||||
import axios from "axios";
|
||||
import config from "../config";
|
||||
import ServiceContext from "../service/service-context";
|
||||
|
||||
const HEADER_CONTENT_TYPE = 'Content-Type';
|
||||
const HEADER_LANGUAGE = 'X-Language';
|
||||
const HEADER_ACCEPT = 'Accept';
|
||||
const HEADER_AUTHORIZATION = 'Authorization';
|
||||
|
||||
export default interface ApiService {
|
||||
get<T>(query: string, url?: string): Observable<T>;
|
||||
@@ -53,6 +55,10 @@ export class DefaultApiService implements ApiService {
|
||||
headers[HEADER_CONTENT_TYPE] = value;
|
||||
}
|
||||
headers[HEADER_ACCEPT] = 'application/json';
|
||||
const token = ServiceContext.auth().getToken();
|
||||
if (token) {
|
||||
headers[HEADER_AUTHORIZATION] = 'Bearer ' + token;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import ApiService, {DefaultApiService} from "./api-service";
|
||||
import {Observable} from "rxjs";
|
||||
|
||||
const AUTH_API_PATH = window.location + 'auth';
|
||||
|
||||
type TokenResponse = { token: string };
|
||||
|
||||
export default interface AuthApiService extends ApiService {
|
||||
authenticate(username: string, password: string): Observable<TokenResponse>;
|
||||
|
||||
refresh(): Observable<TokenResponse>
|
||||
}
|
||||
|
||||
export class DefaultAuthApiService extends DefaultApiService implements AuthApiService {
|
||||
authenticate(username: string, password: string): Observable<{ token: string }> {
|
||||
return this.post<TokenResponse>('/token', {username, password}, AUTH_API_PATH);
|
||||
}
|
||||
|
||||
refresh(): Observable<TokenResponse> {
|
||||
return this.post<TokenResponse>('/refresh', {}, AUTH_API_PATH);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export default function App(props: AppProps) {
|
||||
} else {
|
||||
enqueueSnackbar("Invalid filetype!", {variant: 'error'})
|
||||
}
|
||||
}, [clipboardChannel, serverConfig]);
|
||||
}, [serverConfig, enqueueSnackbar, services]);
|
||||
|
||||
const handleOnWebSearch = useCallback((playlistItem: PlaylistItem): void => {
|
||||
if (playlistItem) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import App from "../../app/app";
|
||||
import Login from "../login/login";
|
||||
import {useServices} from "../../provider/service-provider";
|
||||
|
||||
export default function Authentication(): JSX.Element {
|
||||
|
||||
const services = useServices();
|
||||
const [authenticated, setAuthenticated] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = services.auth().authChannel().subscribe({
|
||||
next: (auth) => setAuthenticated(auth),
|
||||
error: () => setAuthenticated(false),
|
||||
})
|
||||
return () => sub.unsubscribe();
|
||||
}, [services]);
|
||||
|
||||
return <>{authenticated ? <App/> : <Login/>}</>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '../../scss/theme';
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8px;
|
||||
margin: 20% auto auto auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
background-color: var(--app-header-color);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 20px;
|
||||
padding: 8px;
|
||||
width: 300px;
|
||||
|
||||
input {
|
||||
height: 2rem;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
background-color: var(--card-background-color);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 2rem;
|
||||
border: none;
|
||||
font-size: 1.4rem;
|
||||
background-color: var(--text-button-background-color);
|
||||
color: var(--text-button-color);
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, {useCallback, useRef, useState} from 'react';
|
||||
import './login.scss';
|
||||
import {useServices} from "../../provider/service-provider";
|
||||
import {first} from "rxjs/operators";
|
||||
|
||||
const checkUserPwd = (username: string, password: string) => username.trim().length > 0 && password.trim().length > 8;
|
||||
|
||||
export default function Login(): JSX.Element {
|
||||
|
||||
const usernameRef = useRef<HTMLInputElement>();
|
||||
const passwordRef = useRef<HTMLInputElement>();
|
||||
const services = useServices();
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
const handleLogin = useCallback(() => {
|
||||
const username = usernameRef.current.value;
|
||||
const password = passwordRef.current.value;
|
||||
services.auth().authenticate(username, password).pipe(first()).subscribe({
|
||||
next: (auth) => {
|
||||
setAuthorized(auth);
|
||||
},
|
||||
error: () => {
|
||||
setAuthorized(false);
|
||||
}
|
||||
});
|
||||
}, [services]);
|
||||
|
||||
const handleKeyDown = useCallback((event: any) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (checkUserPwd(usernameRef.current.value, passwordRef.current.value)) {
|
||||
handleLogin();
|
||||
}
|
||||
}
|
||||
}, [handleLogin]);
|
||||
|
||||
|
||||
return <div className={'login'}>
|
||||
<div className={'title'}>Login to m3u-filter</div>
|
||||
<form>
|
||||
<div className="login__form">
|
||||
<input ref={usernameRef} type="text" name="username" placeholder="username"/>
|
||||
<input ref={passwordRef} type="password" name="password" placeholder="password"
|
||||
onKeyDown={handleKeyDown}/>
|
||||
<button type="button" className="btn" onClick={handleLogin}>Login</button>
|
||||
<span className={authorized ? '' : 'hidden'}>Failed to login</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.scss';
|
||||
import App from './app/app';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import {ServiceProvider} from "./provider/service-provider";
|
||||
import Authentication from "./component/authentication/authentication";
|
||||
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container);
|
||||
root.render( <SnackbarProvider maxSnack={3} autoHideDuration={1500} anchorOrigin={ ({vertical: 'top', horizontal: 'center'}) }>
|
||||
root.render(
|
||||
<SnackbarProvider maxSnack={3} autoHideDuration={1500} anchorOrigin={ ({vertical: 'top', horizontal: 'center'}) }>
|
||||
<ServiceProvider>
|
||||
<App/>
|
||||
<Authentication/>
|
||||
</ServiceProvider>
|
||||
</SnackbarProvider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import {catchError, map, Observable, ReplaySubject, tap, throwError} from "rxjs";
|
||||
import AuthApiService, {DefaultAuthApiService} from "../api/auth-api-service";
|
||||
|
||||
export default class AuthService {
|
||||
|
||||
private token: string;
|
||||
private subject = new ReplaySubject<boolean>(1);
|
||||
|
||||
constructor(private authApiService: AuthApiService = new DefaultAuthApiService()) {
|
||||
this.subject.next(false);
|
||||
}
|
||||
|
||||
authChannel(): Observable<boolean> {
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
authenticate(username: string, password: string): Observable<boolean> {
|
||||
return this.authApiService.authenticate(username, password).pipe(map(auth => {
|
||||
this.token = auth.token;
|
||||
return auth.token != null
|
||||
}), tap(data => {
|
||||
this.subject.next(data);
|
||||
}), catchError((error:any) => {
|
||||
this.subject.next(false);
|
||||
return throwError(() => error)
|
||||
}));
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.token != null;
|
||||
}
|
||||
|
||||
getToken(): string {
|
||||
return this.token;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import ConfigService from "./config-service";
|
||||
import PlaylistService from "./playlist-service";
|
||||
import FileService from "./file-service";
|
||||
import AuthService from "./auth-service";
|
||||
|
||||
export interface Services {
|
||||
config(): ConfigService;
|
||||
@@ -8,6 +9,8 @@ export interface Services {
|
||||
playlist(): PlaylistService;
|
||||
|
||||
file(): FileService;
|
||||
|
||||
auth(): AuthService;
|
||||
}
|
||||
|
||||
class ServiceContextImpl implements Services {
|
||||
@@ -15,6 +18,7 @@ class ServiceContextImpl implements Services {
|
||||
private readonly _configService: ConfigService = new ConfigService();
|
||||
private readonly _playlistService: PlaylistService = new PlaylistService();
|
||||
private readonly _fileService: FileService = new FileService();
|
||||
private readonly _authService: AuthService = new AuthService();
|
||||
|
||||
config() {
|
||||
return this._configService;
|
||||
@@ -27,6 +31,10 @@ class ServiceContextImpl implements Services {
|
||||
file() {
|
||||
return this._fileService;
|
||||
}
|
||||
|
||||
auth() {
|
||||
return this._authService;
|
||||
}
|
||||
}
|
||||
|
||||
const ServiceContext = new ServiceContextImpl();
|
||||
|
||||
@@ -5,7 +5,7 @@ use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use log::{debug, error};
|
||||
use url::Url;
|
||||
use crate::api::api_model::{AppState, UserApiRequest};
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, UserCredentials};
|
||||
use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigTarget, ConfigInput};
|
||||
use crate::utils::request_utils;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(crate) async fn serve_file(file_path: &Path, req: &HttpRequest, mime_type: m
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest,
|
||||
app_state: &'a web::Data<AppState>) -> Option<(UserCredentials, &'a ConfigTarget)> {
|
||||
app_state: &'a web::Data<AppState>) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
if !username.is_empty() && !password.is_empty() {
|
||||
app_state.config.get_target_for_user(username, password)
|
||||
} else {
|
||||
@@ -36,13 +36,13 @@ pub(crate) fn get_user_target_by_credentials<'a>(username: &str, password: &str,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a web::Data<AppState>) -> Option<(UserCredentials, &'a ConfigTarget)> {
|
||||
pub(crate) fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a web::Data<AppState>) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> {
|
||||
let username = api_req.username.as_str().trim();
|
||||
let password = api_req.password.as_str().trim();
|
||||
get_user_target_by_credentials(username, password, api_req, app_state)
|
||||
}
|
||||
|
||||
pub(crate) fn get_user_server_info(cfg: &Config, user: &UserCredentials) -> ApiProxyServerInfo {
|
||||
pub(crate) fn get_user_server_info(cfg: &Config, user: &ProxyUserCredentials) -> ApiProxyServerInfo {
|
||||
let server_info_list = cfg._api_proxy.read().unwrap().as_ref().unwrap().server.clone();
|
||||
let server_info_name = match &user.server {
|
||||
Some(server_name) => server_name.as_str(),
|
||||
|
||||
+7
-10
@@ -5,8 +5,7 @@ use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, web};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::middleware::{ErrorHandlers, Logger};
|
||||
use actix_web::middleware::{Logger};
|
||||
use log::info;
|
||||
|
||||
use crate::api::api_model::{AppState, DownloadQueue, SharedLocks};
|
||||
@@ -16,7 +15,6 @@ use crate::api::v1_api::v1_api_register;
|
||||
use crate::api::web_index::index_register;
|
||||
use crate::api::xmltv_api::xmltv_api_register;
|
||||
use crate::api::xtream_api::xtream_api_register;
|
||||
use crate::auth::authenticator::handle_unauthorized;
|
||||
use crate::model::config::{Config, ProcessTargets};
|
||||
use crate::processing::playlist_processor;
|
||||
|
||||
@@ -74,7 +72,6 @@ pub(crate) async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>)
|
||||
if web_ui_enabled {
|
||||
info!("Web root: {:?}", &web_dir_path);
|
||||
}
|
||||
let web_auth_enabled = cfg.web_auth_enabled;
|
||||
|
||||
// Web Server
|
||||
HttpServer::new(move || {
|
||||
@@ -87,19 +84,19 @@ pub(crate) async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>)
|
||||
.allow_any_header()
|
||||
.max_age(3600))
|
||||
.app_data(shared_data.clone())
|
||||
.wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized))
|
||||
.configure(|cfg| {
|
||||
// .wrap(Condition::new(web_auth_enabled, ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized)))
|
||||
.configure(|srvcfg| {
|
||||
if web_ui_enabled {
|
||||
cfg.service(actix_files::Files::new("/static", web_dir_path.join("static")));
|
||||
cfg.configure(v1_api_register(web_auth_enabled));
|
||||
srvcfg.service(actix_files::Files::new("/static", web_dir_path.join("static")));
|
||||
srvcfg.configure(v1_api_register(cfg.web_auth.as_ref().unwrap().clone()));
|
||||
}
|
||||
})
|
||||
.configure(xtream_api_register)
|
||||
.configure(m3u_api_register)
|
||||
.configure(xmltv_api_register)
|
||||
.configure(|cfg| {
|
||||
.configure(|srvcfg| {
|
||||
if web_ui_enabled {
|
||||
cfg.configure(index_register(&web_dir_path, web_auth_enabled));
|
||||
srvcfg.configure(index_register(&web_dir_path, cfg.web_auth.as_ref().unwrap().clone()));
|
||||
}
|
||||
})
|
||||
}).bind(format!("{}:{}", host, port))?.run().await
|
||||
|
||||
+4
-3
@@ -11,7 +11,7 @@ use crate::api::download_api;
|
||||
use crate::auth::authenticator::validator;
|
||||
use crate::m3u_filter_error::M3uFilterError;
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, TargetUser};
|
||||
use crate::model::config::{Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, validate_targets};
|
||||
use crate::model::config::{Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, validate_targets, WebAuthConfig};
|
||||
use crate::processing::playlist_processor;
|
||||
use crate::utils::{config_reader, download};
|
||||
|
||||
@@ -212,7 +212,7 @@ pub(crate) async fn config(
|
||||
app_state.config._config_file_path.as_str(),
|
||||
app_state.config._sources_file_path.as_str()) {
|
||||
Ok(mut cfg) => {
|
||||
let _ = cfg.prepare();
|
||||
let _ = cfg.prepare(true);
|
||||
map_config(&cfg)
|
||||
}
|
||||
Err(_) => map_config(&app_state.config)
|
||||
@@ -226,7 +226,8 @@ pub(crate) async fn config(
|
||||
HttpResponse::Ok().json(result)
|
||||
}
|
||||
|
||||
pub(crate) fn v1_api_register(web_auth_enabled: bool) -> impl Fn(&mut web::ServiceConfig) -> () {
|
||||
pub(crate) fn v1_api_register(web_auth_config: WebAuthConfig) -> impl Fn(&mut web::ServiceConfig) -> () {
|
||||
let web_auth_enabled = web_auth_config.enabled;
|
||||
return move |cfg: &mut web::ServiceConfig| {
|
||||
cfg.service(web::scope("/api/v1")
|
||||
.wrap(Condition::new(web_auth_enabled, HttpAuthentication::with_fn(validator)))
|
||||
|
||||
+55
-11
@@ -1,27 +1,71 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix_files::NamedFile;
|
||||
use actix_web::{HttpRequest, web};
|
||||
use actix_web::middleware::Condition;
|
||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
||||
use actix_web::{HttpRequest, HttpResponse, web};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
|
||||
use crate::api::api_model::AppState;
|
||||
use crate::auth::authenticator::validator;
|
||||
use crate::auth::authenticator::{create_jwt, verify_token};
|
||||
use crate::auth::password::verify_password;
|
||||
use crate::auth::user::UserCredential;
|
||||
use crate::model::config::WebAuthConfig;
|
||||
|
||||
async fn index(
|
||||
async fn token(
|
||||
mut req: web::Json<UserCredential>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let username = req.username.as_str();
|
||||
let password = req.password.as_str();
|
||||
|
||||
if username.len() > 0 && password.len() > 0 {
|
||||
let web_auth = app_state.config.web_auth.as_ref().unwrap();
|
||||
if let Some(hash) = web_auth.get_user_password(username) {
|
||||
if verify_password(hash, &password.as_bytes()) {
|
||||
req.zeroize();
|
||||
if let Ok(token) = create_jwt(web_auth) {
|
||||
return HttpResponse::Ok().json(HashMap::from([("token", token)]));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
req.zeroize();
|
||||
HttpResponse::BadRequest().finish()
|
||||
}
|
||||
|
||||
async fn token_refresh(
|
||||
_req: HttpRequest,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> std::io::Result<NamedFile> {
|
||||
let path: PathBuf = [&_app_state.config.api.web_root, "index.html"].iter().collect();
|
||||
NamedFile::open(path)
|
||||
credentials: Option<BearerAuth>,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> HttpResponse {
|
||||
let secret_key = app_state.config.web_auth.as_ref().unwrap().secret.as_ref();
|
||||
if verify_token(credentials, secret_key) {
|
||||
if let Ok(token) = create_jwt(app_state.config.web_auth.as_ref().unwrap()) {
|
||||
return HttpResponse::Ok().json(HashMap::from([("token", token)]));
|
||||
}
|
||||
}
|
||||
HttpResponse::BadRequest().finish()
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn index_register(web_dir_path: &PathBuf, web_auth_enabled: bool) -> impl Fn(&mut web::ServiceConfig) -> () {
|
||||
async fn index(
|
||||
_req: HttpRequest,
|
||||
app_state: web::Data<AppState>,
|
||||
) -> std::io::Result<NamedFile> {
|
||||
let path: PathBuf = [&app_state.config.api.web_root, "index.html"].iter().collect();
|
||||
NamedFile::open(path)
|
||||
}
|
||||
|
||||
pub(crate) fn index_register(web_dir_path: &PathBuf, web_auth_config: WebAuthConfig) -> impl Fn(&mut web::ServiceConfig) -> () {
|
||||
let wdp = web_dir_path.clone();
|
||||
let web_auth_enabled = web_auth_config.enabled;
|
||||
return move |cfg: &mut web::ServiceConfig| {
|
||||
if web_auth_enabled {
|
||||
cfg.service(web::scope("/auth")
|
||||
.route("/token", web::post().to(token))
|
||||
.route("/refresh", web::post().to(token_refresh)));
|
||||
}
|
||||
cfg.service(web::scope("/")
|
||||
.wrap(Condition::new(web_auth_enabled, HttpAuthentication::with_fn(validator)))
|
||||
.route("", web::get().to(index))
|
||||
.service(actix_files::Files::new("/", &wdp)));
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use url::Url;
|
||||
|
||||
use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo};
|
||||
use crate::api::api_utils::{get_user_server_info, get_user_target, get_user_target_by_credentials, serve_file, stream_response};
|
||||
use crate::model::api_proxy::{ProxyType, UserCredentials};
|
||||
use crate::model::api_proxy::{ProxyType, ProxyUserCredentials};
|
||||
use crate::model::config::{Config, ConfigInput, InputType};
|
||||
use crate::model::config::TargetType;
|
||||
use crate::model::playlist::XtreamCluster;
|
||||
@@ -112,7 +112,7 @@ fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &str, action_p
|
||||
}
|
||||
|
||||
|
||||
fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationResponse {
|
||||
fn get_user_info(user: &ProxyUserCredentials, cfg: &Config) -> XtreamAuthorizationResponse {
|
||||
let server_info = get_user_server_info(cfg, user);
|
||||
|
||||
let now = Local::now();
|
||||
@@ -262,7 +262,7 @@ async fn xtream_get_stream_info(app_state: &AppState, target_name: &str, stream_
|
||||
Err(Error::new(std::io::ErrorKind::Other, format!("Cant find stream with id: {}/{}/{}", target_name, &cluster, stream_id)))
|
||||
}
|
||||
|
||||
async fn xtream_get_stream_info_response(app_state: &AppState, user: &UserCredentials,
|
||||
async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials,
|
||||
target_name: &str, stream_id: &str,
|
||||
cluster: &XtreamCluster) -> HttpResponse {
|
||||
match FromStr::from_str(stream_id) {
|
||||
@@ -284,7 +284,7 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &UserCreden
|
||||
}
|
||||
}
|
||||
|
||||
async fn xtream_get_short_epg(app_state: &AppState, user: &UserCredentials, target_name: &str, stream_id: &str, limit: &str) -> HttpResponse {
|
||||
async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target_name: &str, stream_id: &str, limit: &str) -> HttpResponse {
|
||||
if !stream_id.is_empty() {
|
||||
if let Some(target_input) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) {
|
||||
if let Some(action_url) = get_xtream_player_api_action_url(target_input, "get_short_epg") {
|
||||
|
||||
+55
-15
@@ -1,23 +1,63 @@
|
||||
use actix_web::{dev::ServiceRequest, Error, HttpResponse};
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use actix_web::middleware::ErrorHandlerResponse;
|
||||
use actix_web::{dev::ServiceRequest, Error, web};
|
||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||
use log::info;
|
||||
use chrono::{Local, Duration};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, encode, decode, EncodingKey, Header, Validation};
|
||||
use crate::api::api_model::AppState;
|
||||
use crate::model::config::WebAuthConfig;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Claims {
|
||||
iss: String,
|
||||
iat: i64,
|
||||
exp: i64,
|
||||
}
|
||||
|
||||
pub(crate) fn create_jwt(web_auth_config: &WebAuthConfig) -> Result<String, std::io::Error> {
|
||||
let mut header = Header::new(Algorithm::HS256);
|
||||
header.typ = Some("JWT".to_string());
|
||||
let now = Local::now();
|
||||
let iat = now.timestamp();
|
||||
let exp = (now + Duration::minutes(30)).timestamp();
|
||||
let claims = Claims {
|
||||
iss: web_auth_config.issuer.clone(),
|
||||
iat,
|
||||
exp,
|
||||
};
|
||||
match encode(&header, &claims, &EncodingKey::from_secret(web_auth_config.secret.as_bytes())) {
|
||||
Ok(jwt) => Ok(jwt),
|
||||
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn verify_token(bearer: Option<BearerAuth>, secret_key: &[u8]) -> bool {
|
||||
if let Some(auth) = bearer {
|
||||
let token = auth.token();
|
||||
let token_message = decode::<Claims>(&token, &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256));
|
||||
if let Ok(_) = token_message {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) async fn validator(
|
||||
req: ServiceRequest,
|
||||
credentials: Option<BearerAuth>,
|
||||
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
|
||||
info!("{:?}", credentials);
|
||||
// Ok(req)
|
||||
Err((actix_web::error::ErrorUnauthorized("Unauthorized"), req))
|
||||
let app_state: &web::Data<AppState> = req.app_data::<web::Data<AppState>>().unwrap();
|
||||
let secret_key = app_state.config.web_auth.as_ref().unwrap().secret.as_ref();
|
||||
if verify_token(credentials, secret_key) {
|
||||
Ok(req)
|
||||
} else {
|
||||
Err((actix_web::error::ErrorUnauthorized("Unauthorized"), req))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_unauthorized<B>(srvres: ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
let (req, _) = srvres.into_parts();
|
||||
let resp = HttpResponse::TemporaryRedirect().insert_header(("Location", "login")).finish();
|
||||
let result = ServiceResponse::new(req, resp)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body();
|
||||
Ok(ErrorHandlerResponse::Response(result))
|
||||
}
|
||||
// pub(crate) fn handle_unauthorized<B>(srvres: ServiceResponse<B>) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
// let (req, _) = srvres.into_parts();
|
||||
// let resp = HttpResponse::TemporaryRedirect().insert_header(("Location", "/auth/login")).finish();
|
||||
// let result = ServiceResponse::new(req, resp)
|
||||
// .map_into_boxed_body()
|
||||
// .map_into_right_body();
|
||||
// Ok(ErrorHandlerResponse::Response(result))
|
||||
// }
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
pub(crate) mod authenticator;
|
||||
mod password;
|
||||
pub(crate) mod password;
|
||||
pub(crate) mod user;
|
||||
+44
-24
@@ -1,24 +1,44 @@
|
||||
// use argon2::{
|
||||
// password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||
// Argon2,
|
||||
// };
|
||||
// use log::error;
|
||||
//
|
||||
// pub async fn hash(password: &[u8]) -> Option<String> {
|
||||
// let salt = SaltString::generate(&mut OsRng);
|
||||
// match Argon2::default().hash_password(password, &salt) {
|
||||
// Ok(pwd) => Some(pwd.to_string()),
|
||||
// Err(err) => {
|
||||
// error!("Failed to hash password {}", err);
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// pub fn verify_password(hash: &str, password: &[u8]) -> bool {
|
||||
// let parsed_hash = PasswordHash::new(hash)?;
|
||||
// match Argon2::default().verify_password(password, &parsed_hash) {
|
||||
// Ok(_) => true,
|
||||
// Err(_) => false
|
||||
// }
|
||||
// }
|
||||
use std::io::ErrorKind;
|
||||
use rand::{Rng, distributions::Alphanumeric, rngs::OsRng};
|
||||
|
||||
fn generate_salt(length: usize) -> String {
|
||||
let rng = OsRng;
|
||||
let salt: String = rng
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(length)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
salt
|
||||
}
|
||||
|
||||
pub(crate) fn hash(password: &[u8]) -> Option<String> {
|
||||
let salt = generate_salt(64);
|
||||
if password.len() > 0 {
|
||||
let config = argon2::Config::default();
|
||||
if let Ok(hash) = argon2::hash_encoded(password, salt.as_bytes(), &config) {
|
||||
return Some(hash);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn verify_password(hash: &str, password: &[u8]) -> bool {
|
||||
if let Ok(valid) = argon2::verify_encoded(hash, password) {
|
||||
return valid;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn generate_password() -> std::io::Result<String> {
|
||||
match rpassword::prompt_password("password> ") {
|
||||
Ok(pwd) => {
|
||||
match hash(pwd.as_bytes()) {
|
||||
None => Err(std::io::Error::new(ErrorKind::Other, "Failed to generate hash")),
|
||||
Some(hash) => Ok(hash),
|
||||
}
|
||||
},
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::ptr;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct UserCredential {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
|
||||
impl UserCredential {
|
||||
pub(crate) fn zeroize(&mut self) {
|
||||
unsafe {
|
||||
let password_ptr = self.password.as_mut_ptr();
|
||||
let password_len = self.password.len();
|
||||
ptr::write_bytes(password_ptr, 0, password_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -2,6 +2,7 @@ extern crate env_logger;
|
||||
extern crate pest;
|
||||
#[macro_use]
|
||||
extern crate pest_derive;
|
||||
extern crate core;
|
||||
|
||||
use std::sync::Arc;
|
||||
use actix_rt::System;
|
||||
@@ -9,6 +10,7 @@ use actix_rt::System;
|
||||
use clap::Parser;
|
||||
use env_logger::Builder;
|
||||
use log::{error, info, LevelFilter};
|
||||
use crate::auth::password::generate_password;
|
||||
|
||||
use crate::model::config::{Config, ProcessTargets, validate_targets};
|
||||
use crate::processing::playlist_processor;
|
||||
@@ -31,7 +33,6 @@ mod auth;
|
||||
#[command(version)]
|
||||
#[command(about = "Extended M3U playlist filter", long_about = None)]
|
||||
struct Args {
|
||||
|
||||
/// The config directory
|
||||
#[arg(short = 'p', long = "config-path")]
|
||||
config_path: Option<String>,
|
||||
@@ -48,7 +49,6 @@ struct Args {
|
||||
#[arg(short = 'm', long = "mapping")]
|
||||
mapping_file: Option<String>,
|
||||
|
||||
|
||||
/// The target to process
|
||||
#[arg(short = 't', long)]
|
||||
target: Option<Vec<String>>,
|
||||
@@ -64,6 +64,11 @@ struct Args {
|
||||
/// log level
|
||||
#[arg(short = 'l', long = "log-level", default_missing_value = "info")]
|
||||
log_level: Option<String>,
|
||||
|
||||
/// log level
|
||||
#[arg(short = None, long = "genpwd", default_value_t = false, default_missing_value = "true")]
|
||||
genpwd: bool,
|
||||
|
||||
}
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -76,9 +81,20 @@ fn main() {
|
||||
let config_file: String = args.config_file.unwrap_or(file_utils::get_default_config_file_path(&config_path));
|
||||
let sources_file: String = args.source_file.unwrap_or(file_utils::get_default_sources_file_path(&config_path));
|
||||
|
||||
|
||||
let mut cfg = config_reader::read_config(config_path.as_str(), config_file.as_str(), sources_file.as_str()).unwrap_or_else(|err| exit!("{}", err));
|
||||
|
||||
if args.genpwd {
|
||||
match generate_password() {
|
||||
Ok(pwd) => {
|
||||
println!("{}", pwd);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// this does not work
|
||||
// if args.log_level.is_none() {
|
||||
// if let Some(log_level) = &cfg.log_level {
|
||||
@@ -115,8 +131,7 @@ fn start_in_cli_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
fn start_in_server_mode(cfg: Arc<Config>, targets: Arc<ProcessTargets>) {
|
||||
info!("Server running: http://{}:{}", &cfg.api.host, &cfg.api.port);
|
||||
match api::main_api::start_server(cfg, targets) {
|
||||
Ok(_) => {
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
exit!("Can't start server: {}", e);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ impl FromStr for ProxyType {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct UserCredentials {
|
||||
pub(crate) struct ProxyUserCredentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub token: Option<String>,
|
||||
@@ -56,7 +56,7 @@ pub(crate) struct UserCredentials {
|
||||
pub server: Option<String>,
|
||||
}
|
||||
|
||||
impl UserCredentials {
|
||||
impl ProxyUserCredentials {
|
||||
|
||||
pub fn prepare(&mut self, resolve_var: bool) {
|
||||
if resolve_var {
|
||||
@@ -95,15 +95,15 @@ impl UserCredentials {
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct TargetUser {
|
||||
pub target: String,
|
||||
pub credentials: Vec<UserCredentials>,
|
||||
pub credentials: Vec<ProxyUserCredentials>,
|
||||
}
|
||||
|
||||
impl TargetUser {
|
||||
pub fn get_target_name(&self, username: &str, password: &str) -> Option<(&UserCredentials, &str)> {
|
||||
pub fn get_target_name(&self, username: &str, password: &str) -> Option<(&ProxyUserCredentials, &str)> {
|
||||
self.credentials.iter().find(|c| c.matches(username, password))
|
||||
.map(|credentials| (credentials, self.target.as_str()))
|
||||
}
|
||||
pub fn get_target_name_by_token(&self, token: &str) -> Option<(&UserCredentials, &str)> {
|
||||
pub fn get_target_name_by_token(&self, token: &str) -> Option<(&ProxyUserCredentials, &str)> {
|
||||
self.credentials.iter().find(|c| c.matches_token(token))
|
||||
.map(|credentials| (credentials, self.target.as_str()))
|
||||
}
|
||||
@@ -232,7 +232,7 @@ impl ApiProxyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_target_name(&self, username: &str, password: &str) -> Option<(UserCredentials, String)> {
|
||||
pub fn get_target_name(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, String)> {
|
||||
for target_user in &self.user {
|
||||
if let Some((credentials, target_name)) = target_user.get_target_name(username, password) {
|
||||
return Some((credentials.clone(), target_name.to_string()));
|
||||
@@ -241,7 +241,7 @@ impl ApiProxyConfig {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_target_name_by_token(&self, token: &str) -> Option<(UserCredentials, String)> {
|
||||
pub fn get_target_name_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, String)> {
|
||||
for target_user in &self.user {
|
||||
if let Some((credentials, target_name)) = target_user.get_target_name_by_token(token) {
|
||||
return Some((credentials.clone(), target_name.to_string()));
|
||||
|
||||
+78
-8
@@ -2,20 +2,23 @@ use std::rc::Rc;
|
||||
use enum_iterator::Sequence;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs::File;
|
||||
use std::io::BufRead;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use log::{debug, error, warn};
|
||||
use path_absolutize::*;
|
||||
use crate::auth::user::UserCredential;
|
||||
|
||||
use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider};
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::messaging::MsgKind;
|
||||
use crate::model::api_proxy::{ApiProxyConfig, UserCredentials};
|
||||
use crate::model::api_proxy::{ApiProxyConfig, ProxyUserCredentials};
|
||||
use crate::model::mapping::Mapping;
|
||||
use crate::model::mapping::Mappings;
|
||||
use crate::utils::file_utils;
|
||||
use crate::utils::{config_reader, file_utils};
|
||||
|
||||
pub(crate) const MAPPER_ATTRIBUTE_FIELDS: &[&str] = &[
|
||||
"name", "title", "group", "id", "logo",
|
||||
@@ -728,6 +731,67 @@ impl ConfigDto {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct WebAuthConfig {
|
||||
#[serde(default = "default_as_true")]
|
||||
pub enabled: bool,
|
||||
pub issuer: String,
|
||||
pub secret: String,
|
||||
pub userfile: String,
|
||||
pub _users: Option<Vec<UserCredential>>,
|
||||
}
|
||||
|
||||
impl WebAuthConfig {
|
||||
pub fn prepare(&mut self, config_path: &str, resolve_var: bool) -> Result<(), M3uFilterError> {
|
||||
if resolve_var {
|
||||
self.issuer = config_reader::resolve_env_var(&self.issuer);
|
||||
self.secret = config_reader::resolve_env_var(&self.secret);
|
||||
self.userfile = config_reader::resolve_env_var(&self.userfile);
|
||||
}
|
||||
|
||||
let userfile_path = PathBuf::from(file_utils::get_default_file_path(config_path, &self.userfile));
|
||||
if !file_utils::path_exists(&userfile_path) {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not find userfile {:?}", &userfile_path);
|
||||
}
|
||||
|
||||
if let Ok(file) = File::open(&userfile_path) {
|
||||
let mut users = vec![];
|
||||
let reader = std::io::BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
match line {
|
||||
Ok(credential) => {
|
||||
let mut parts = credential.split(':');
|
||||
if let (Some(username), Some(password)) = (parts.next(), parts.next()) {
|
||||
users.push(UserCredential {
|
||||
username: username.trim().to_string(),
|
||||
password: password.trim().to_string(),
|
||||
});
|
||||
debug!("Read ui user {}", username);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
self._users = Some(users);
|
||||
} else {
|
||||
return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Could not read userfile {:?}", &userfile_path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user_password(&self, username: &str) -> Option<&str> {
|
||||
if let Some(users) = &self._users {
|
||||
for credential in users {
|
||||
if credential.username.eq_ignore_ascii_case(username) {
|
||||
return Some(credential.password.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct Config {
|
||||
#[serde(default = "default_as_zero")]
|
||||
@@ -743,8 +807,7 @@ pub(crate) struct Config {
|
||||
pub update_on_boot: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
pub web_ui_enabled: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
pub web_auth_enabled: bool,
|
||||
pub web_auth: Option<WebAuthConfig>,
|
||||
pub messaging: Option<MessagingConfig>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub _api_proxy: Arc<RwLock<Option<ApiProxyConfig>>>,
|
||||
@@ -764,7 +827,7 @@ impl Config {
|
||||
self._api_proxy = Arc::new(RwLock::new(api_proxy));
|
||||
}
|
||||
|
||||
fn _get_target_for_user(&self, user_target: Option<(UserCredentials, String)>) -> Option<(UserCredentials, &ConfigTarget)> {
|
||||
fn _get_target_for_user(&self, user_target: Option<(ProxyUserCredentials, String)>) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
match user_target {
|
||||
Some((user, target_name)) => {
|
||||
for source in &self.sources {
|
||||
@@ -787,7 +850,7 @@ impl Config {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(UserCredentials, &ConfigTarget)> {
|
||||
pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
match self._api_proxy.read().unwrap().as_ref() {
|
||||
Some(api_proxy) => {
|
||||
self._get_target_for_user(api_proxy.get_target_name(username, password))
|
||||
@@ -796,7 +859,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(UserCredentials, &ConfigTarget)> {
|
||||
pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> {
|
||||
match self._api_proxy.read().unwrap().as_ref() {
|
||||
Some(api_proxy) => {
|
||||
self._get_target_for_user(api_proxy.get_target_name_by_token(token))
|
||||
@@ -836,7 +899,7 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self) -> Result<(), M3uFilterError> {
|
||||
pub fn prepare(&mut self, resolve_var: bool) -> Result<(), M3uFilterError> {
|
||||
self.working_dir = file_utils::get_working_path(&self.working_dir);
|
||||
if self.backup_dir.is_none() {
|
||||
self.backup_dir = Some(PathBuf::from(&self.working_dir).join(".backup").into_os_string().to_string_lossy().to_string());
|
||||
@@ -902,6 +965,13 @@ impl Config {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(web_auth) = &mut self.web_auth {
|
||||
if let Err(err) = web_auth.prepare(&self._config_path, resolve_var) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use log::error;
|
||||
use crate::{create_m3u_filter_error_result};
|
||||
use crate::api::api_utils::get_user_server_info;
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind};
|
||||
use crate::model::api_proxy::UserCredentials;
|
||||
use crate::model::api_proxy::ProxyUserCredentials;
|
||||
use crate::model::config::{Config, ConfigTarget};
|
||||
use crate::model::playlist::{PlaylistGroup, PlaylistItemType};
|
||||
use crate::processing::m3u_parser::consume_m3u;
|
||||
@@ -240,7 +240,7 @@ pub(crate) fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playl
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn rewrite_m3u_playlist(cfg: &Config, target: &ConfigTarget, user: &UserCredentials) -> Option<String> {
|
||||
pub(crate) fn rewrite_m3u_playlist(cfg: &Config, target: &ConfigTarget, user: &ProxyUserCredentials) -> Option<String> {
|
||||
let filename = target.get_m3u_filename();
|
||||
if filename.is_some() {
|
||||
if let Some((m3u_path, url_path, idx_path)) = get_m3u_file_paths(cfg, &filename) {
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) fn read_config(config_path: &str, config_file: &str, sources_file: &s
|
||||
result._config_path = config_path.to_string();
|
||||
result._config_file_path = config_file.to_string();
|
||||
result._sources_file_path = sources_file.to_string();
|
||||
match result.prepare() {
|
||||
match result.prepare(true) {
|
||||
Ok(_) => Ok(result),
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn get_default_path(file: &str) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_default_file_path(config_path: &str, file: &str) -> String {
|
||||
pub(crate) fn get_default_file_path(config_path: &str, file: &str) -> String {
|
||||
let path: PathBuf = PathBuf::from(config_path);
|
||||
let default_path = path.join(file);
|
||||
String::from(if default_path.exists() {
|
||||
|
||||
Reference in New Issue
Block a user