import * as randomstring from 'randomstring' ; import * as uuidv4 from 'uuid/v4' ; import { Clock } from '../infrastructure/clock' ; import { isPortUsed } from '../infrastructure/get_port' ; import { JsonConfig } from '../infrastructure/json_config' ; import * as logging from '../infrastructure/logging' ; import { PrometheusClient } from '../infrastructure/prometheus_scraper' ; import { AccessKey , AccessKeyId , AccessKeyMetricsId , AccessKeyRepository , DataLimit , ProxyParams } from '../model/access_key' ; import * as errors from '../model/errors' ; import { ShadowsocksServer } from '../model/shadowsocks_server' ; import { PrometheusManagerMetrics } from './manager_metrics' ; interface AccessKeyStorageJson { id : AccessKeyId ; metricsId : AccessKeyId ; name : string ; password : string ; port : number ; encryptionMethod ? : string ; dataLimit ? : DataLimit ; } export interface AccessKeyConfigJson { accessKeys ? : AccessKeyStorageJson [ ] ; nextId ? : number ; } class ServerAccessKey implements AccessKey { public isOverDataLimit = false ; constructor ( readonly id , public name , public metricsId , readonly proxyParams , public dataLimit ? ) { } } function generatePassword ( ) { return randomstring . generate ( 12 ) ; } function makeAccessKey ( hostname , accessKeyJson ) { const proxyParams = { hostname , portNumber : accessKeyJson . port , encryptionMethod : accessKeyJson . encryptionMethod , password : accessKeyJson . password , } ; return new ServerAccessKey ( accessKeyJson . id , accessKeyJson . name , accessKeyJson . metricsId , proxyParams , accessKeyJson . dataLimit ) ; } function accessKeyToStorageJson ( accessKey ) { return { id : accessKey . id , metricsId : accessKey . metricsId , name : accessKey . name , password : accessKey . proxyParams . password , port : accessKey . proxyParams . portNumber , encryptionMethod : accessKey . proxyParams . encryptionMethod , dataLimit : accessKey . dataLimit } ; } export class ServerAccessKeyRepository implements AccessKeyRepository { private static DATA_LIMITS_ENFORCEMENT_INTERVAL_MS = 60 * 60 * 1000 ; private NEW_USER_ENCRYPTION_METHOD = 'chacha20-ietf-poly1305' ; private accessKeys : ServerAccessKey [ ] ; constructor ( private portForNewAccessKeys , private proxyHostname , private keyConfig , private shadowsocksServer , private prometheusClient , private _defaultDataLimit ? ) { if ( this . keyConfig . data ( ) . accessKeys === undefined ) { this . keyConfig . data ( ) . accessKeys = [ ] ; } if ( this . keyConfig . data ( ) . nextId === undefined ) { this . keyConfig . data ( ) . nextId = 0 ; } this . accessKeys = this . loadAccessKeys ( ) ; } async start ( clock ) { const tryEnforceDataLimits = async ( ) => { try { await this . enforceAccessKeyDataLimits ( ) ; } catch ( e ) { logging . error ( `Failed to enforce access key limits: ${ e }` ) ; } } ; await tryEnforceDataLimits ( ) ; await this . updateServer ( ) ; clock . setInterval ( tryEnforceDataLimits , ServerAccessKeyRepository . DATA_LIMITS_ENFORCEMENT_INTERVAL_MS ) ; } private isExistingAccessKeyPort ( port ) { return this . accessKeys . some ( ( key ) => { return key . proxyParams . portNumber === port ; } ) ; } setHostname ( hostname ) { this . proxyHostname = hostname ; } async setPortForNewAccessKeys ( port ) { if ( ! Number . isInteger ( port ) || port < 1 || port > 65535 ) { throw new errors . InvalidPortNumber ( port . toString ( ) ) ; } if ( ! this . isExistingAccessKeyPort ( port ) && await isPortUsed ( port ) ) { throw new errors . PortUnavailable ( port ) ; } this . portForNewAccessKeys = port ; } async createNewAccessKey ( ) { const id = this . keyConfig . data ( ) . nextId . toString ( ) ; this . keyConfig . data ( ) . nextId += 1 ; const metricsId = uuidv4 ( ) ; const password = generatePassword ( ) ; const proxyParams = { hostname : this . proxyHostname , portNumber : this . portForNewAccessKeys , encryptionMethod : this . NEW_USER_ENCRYPTION_METHOD , password , } ; const accessKey = new ServerAccessKey ( id , '' , metricsId , proxyParams ) ; this . accessKeys . push ( accessKey ) ; this . saveAccessKeys ( ) ; await this . updateServer ( ) ; return accessKey ; } removeAccessKey ( id ) { for ( let ai = 0 ; ai < this . accessKeys . length ; ai ++ ) { const accessKey = this . accessKeys [ ai ] ; if ( accessKey . id === id ) { this . accessKeys . splice ( ai , 1 ) ; this . saveAccessKeys ( ) ; this . updateServer ( ) ; return ; } } throw new errors . AccessKeyNotFound ( id ) ; } listAccessKeys ( ) { return [ ... this . accessKeys ] ; } renameAccessKey ( id , name ) { const accessKey = this . getAccessKey ( id ) ; accessKey . name = name ; this . saveAccessKeys ( ) ; } setAccessKeyDataLimit ( id , limit ) { this . getAccessKey ( id ) . dataLimit = limit ; this . saveAccessKeys ( ) ; this . enforceAccessKeyDataLimits ( ) ; } removeAccessKeyDataLimit ( id ) { delete this . getAccessKey ( id ) . dataLimit ; this . saveAccessKeys ( ) ; this . enforceAccessKeyDataLimits ( ) ; } get defaultDataLimit ( ) : DataLimit | undefined { return this . _defaultDataLimit ; } setDefaultDataLimit ( limit ) { this . _defaultDataLimit = limit ; this . enforceAccessKeyDataLimits ( ) ; } removeDefaultDataLimit ( ) { delete this . _defaultDataLimit ; this . enforceAccessKeyDataLimits ( ) ; } getMetricsId ( id ) { const accessKey = this . getAccessKey ( id ) ; return accessKey ? accessKey . metricsId : undefined ; } async enforceAccessKeyDataLimits ( ) { const metrics = new PrometheusManagerMetrics ( this . prometheusClient ) ; const bytesTransferredById = ( await metrics . getOutboundByteTransfer ( { hours : 30 * 24 } ) ) . bytesTransferredByUserId ; let limitStatusChanged = false ; for ( const accessKey of this . accessKeys ) { const usageBytes = bytesTransferredById [ accessKey . id ] ?? 0 ; const wasOverDataLimit = accessKey . isOverDataLimit ; let limitBytes = ( accessKey . dataLimit ?? this . _defaultDataLimit ) ?. bytes ; if ( limitBytes === undefined ) { limitBytes = Number . POSITIVE_INFINITY ; } accessKey . isOverDataLimit = usageBytes > limitBytes ; limitStatusChanged = accessKey . isOverDataLimit !== wasOverDataLimit || limitStatusChanged ; } if ( limitStatusChanged ) { await this . updateServer ( ) ; } } private updateServer ( ) { const serverAccessKeys = this . accessKeys . filter ( key => ! key . isOverDataLimit ) . map ( key => { return { id : key . id , port : key . proxyParams . portNumber , cipher : key . proxyParams . encryptionMethod , secret : key . proxyParams . password } ; } ) ; return this . shadowsocksServer . update ( serverAccessKeys ) ; } private loadAccessKeys ( ) { return this . keyConfig . data ( ) . accessKeys . map ( key => makeAccessKey ( this . proxyHostname , key ) ) ; } private saveAccessKeys ( ) { this . keyConfig . data ( ) . accessKeys = this . accessKeys . map ( key => accessKeyToStorageJson ( key ) ) ; this . keyConfig . write ( ) ; } private getAccessKey ( id ) { for ( const accessKey of this . accessKeys ) { if ( accessKey . id === id ) { return accessKey ; } } throw new errors . AccessKeyNotFound ( id ) ; } }