classes.ps1
class adminPage{ [string] $page [hashtable[]] $requestParams [hashtable] $links [object] $response [string] $tokensPath [string] $currentToken hidden [string] $adminCommand adminPage($page,$requestParams,$tokensPath,$currentToken) { $this.requestParams = $requestParams $this.page = $page $this.tokensPath = $tokensPath $this.currentToken = $currentToken switch -Wildcard ($this.page) { '/stop' {$this.stop()} '/clearcache' {$this.clearcache()} "/user*" {$this.user()} default {$this.default()} } } [void] makeLinks([string]$thisPage,[array]$children) { $this.links = @{ this = $thisPage children = $children parent = '/admin' } } [void] stop() { $this.adminCommand = 'stop' $this.makeLinks('/admin/stop',$null) $items = [pscustomobject] @{ message = 'Server requested to stop listening' } #$this.response = New-Object adminResponse -ArgumentList ($items,$this.links,$null) $this.response = [adminResponse]::NEW($items,$this.links,$null) } [void] clearcache() { $this.adminCommand = 'clearcache' $this.makeLinks('/admin/clearcache',$null) $items = [pscustomobject] @{ message = 'Clear Cache Initialised' } #$this.response = New-Object adminResponse -ArgumentList ($items,$this.links,$null) $this.response = [adminResponse]::NEW($items,$this.links,$null) } [void] default() { $children = @( '/stop', '/user/', '/user/new', '/user/disable', '/user/enable', '/user/revokeAdmin' '/user/grantAdmin' '/user/get', '/clearcache' ) $this.makeLinks('/admin/',$children) $this.links.parent = '/' #$this.response = New-Object adminResponse -ArgumentList ($null,$this.links,$null) $this.response = [adminResponse]::NEW($null,$this.links,$null) } [void] user() { switch ($this.page) { '/user/new' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The associated username' datatype = 'string' }, [pscustomobject] @{ name = 'isadmin' description = 'Should the user have elevated rights' datatype = 'bool' } ) $this.makeLinks('/admin/user/new',$null) $username = $this.requestParams.username if($username) { $adminValue = $this.requestParams.isAdmin $acceptedValues = @(1,'true','yes','t','y') try{ $currentTokens = Import-Clixml $this.tokensPath -ErrorAction Stop if($($currentTokens|measure-Object).count -eq 1 ) { #We have a single item and we need an array write-verbose 'Converting To Array' $currentTokens = @($currentTokens) } }catch{ $currentTokens = @() } if($adminValue -in $acceptedValues) { write-verbose "$username will be created with Admin access" $admin = $true }else{ write-verbose "$username will be created with Std access" $admin = $false } try{ write-verbose 'Got the tokens' $guid = $(new-guid).Guid $h = [pscustomobject]@{event='created';by="$($this.currentToken)";date="$(get-date -format s)"} write-verbose 'Making user object' $newUser = [pscustomobject] @{ token = $guid username = $username isadmin = $admin history = [array] @($h) enabled = $true } write-verbose 'Adding Token to the list' $currentTokens += $newUser #Look, I know this is not the best way, but it works write-verbose $($currentTokens|ConvertTo-Json -Depth 2) write-verbose 'Exporting' $currentTokens|Export-Clixml $this.tokensPath -Force $this.adminCommand = 'updateUsers' }catch{ $currentTokens = $null write-verbose 'DID NOT GET THE TOKENS' $newUser = $null } }else{ $newUser = $null } #$this.response = new-object adminResponse -ArgumentList ($newUser,$this.links,$inputs) $this.response = [adminResponse]::NEW($newUser,$this.links,$inputs) } '/user/disable' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The username - to disable all associated tokens' datatype = 'string' }, [pscustomobject] @{ name = 'token' description = 'Token to disable - not used if Username is specified' datatype = 'string' } ) $this.makeLinks('/admin/user/disable',$null) $username = $this.requestParams.username $token = $this.requestParams.token if($username) { $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.username -eq $username) { $user.enabled = $false $user.history += [pscustomobject]@{event='disabled';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }elseIf($token){ $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.token -eq $token) { $user.enabled = $false $user.history += [pscustomobject]@{event='disabled';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }else{ $return = $null } #$this.response = new-object adminResponse -ArgumentList ($return,$this.links,$inputs) $this.response = [adminResponse]::NEW($return,$this.links,$inputs) $this.adminCommand = 'updateUsers' } '/user/enable' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The username - to enable all associated tokens' datatype = 'string' }, [pscustomobject] @{ name = 'token' description = 'Token to enable - not used if Username is specified' datatype = 'string' } ) $this.makeLinks('/admin/user/enable',$null) $username = $this.requestParams.username $token = $this.requestParams.token if($username) { $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.username -eq $username) { $user.enabled = $true $user.history += [pscustomobject]@{event='enabled';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }elseIf($token){ $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.token -eq $token) { $user.enabled = $true $user.history += [pscustomobject]@{event='enabled';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }else{ $return = $null } #$this.response = new-object adminResponse -ArgumentList ($return,$this.links,$inputs) $this.response = [adminResponse]::NEW($return,$this.links,$inputs) $this.adminCommand = 'updateUsers' } '/user/revokeAdmin' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The username - to revoke all associated tokens' datatype = 'string' }, [pscustomobject] @{ name = 'token' description = 'Token to revoke Admin - not used if Username is specified' datatype = 'string' } ) $this.makeLinks('/admin/user/revokeAdmin',$null) $username = $this.requestParams.username $token = $this.requestParams.token if($username) { $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.username -eq $username -and $user.isadmin -eq $true) { $user.isadmin = $false $user.history += [pscustomobject]@{event='adminRevoked';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }elseIf($token){ $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.token -eq $token -and $user.isadmin -eq $true) { $user.isadmin = $false $user.history += [pscustomobject]@{event='adminRevoked';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }else{ $return = $null } #$this.response = new-object adminResponse -ArgumentList ($return,$this.links,$inputs) $this.response = [adminResponse]::NEW($return,$this.links,$inputs) $this.adminCommand = 'updateUsers' } '/user/grantAdmin' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The username - to elevate all associated tokens' datatype = 'string' }, [pscustomobject] @{ name = 'token' description = 'Token to elevate - not used if Username is specified' datatype = 'string' } ) $this.makeLinks('/admin/user/grantAdmin',$null) $username = $this.requestParams.username $token = $this.requestParams.token if($username) { $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.username -eq $username -and $user.isadmin -eq $false) { $user.isadmin = $true $user.history += [pscustomobject]@{event='adminGranted';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }elseIf($token){ $currentTokens = Import-Clixml $this.tokensPath $return = foreach($user in $currentTokens) { if($user.token -eq $token -and $user.isadmin -eq $false) { $user.isadmin = $true $user.history += [pscustomobject]@{event='adminRevoked';by=$this.currentToken;date=$(get-date -format s)} $user } } $currentTokens | Export-Clixml $this.tokensPath -Force }else{ $return = $null } #$this.response = new-object adminResponse -ArgumentList ($return,$this.links,$inputs) $this.response = [adminResponse]::NEW($return,$this.links,$inputs) $this.adminCommand = 'updateUsers' } '/user/get' { $inputs = @( [pscustomobject] @{ name = 'username' description = 'The username to search for - accepts partial match' datatype = 'string' }, [pscustomobject] @{ name = 'token' description = 'Token to search for - not used if Username is specified' datatype = 'string' }, [pscustomobject] @{ name = 'default' description = 'If username/token not specified, all users will be retrieved' datatype = 'none' } ) $this.makeLinks('/admin/user/get',$null) $username = $this.requestParams.username $token = $this.requestParams.token $currentTokens = Import-Clixml $this.tokensPath if($username) { $return = foreach($user in $currentTokens) { if($user.username -eq $username ) { $user } } }elseIf($token){ $return = foreach($user in $currentTokens) { if($user.token -eq $token -and $user.isadmin -eq $false) { $user } } }else{ $return = $currentTokens } #$this.response = new-object adminResponse -ArgumentList ($return,$this.links,$inputs) $this.response = [adminResponse]::NEW($return,$this.links,$inputs) } '/user/' { $children = @( '/new', '/get', '/grantAdmin', '/revokeAdmin', '/new', '/disable', '/enable' ) $this.makeLinks('/admin/user/',$children) #$this.response = New-Object adminResponse -ArgumentList ($null,$this.links,$null) $this.response = [adminResponse]::NEW($null,$this.links,$null) } } } } class cacheObject { [pageResponse]$response [datetime]$expires cacheObject([pageResponse]$response,[int]$cacheTime) { $this.response = $response $this.expires = $(get-date).AddMinutes($cacheTime) } } class ipAssist { [string]$cidr [string]$subnetMask [int]$netBits [string]$networkId [string]$firstIpAddress [string]$lastIpAddress [long]$hostsPerNet [long]$startInteger [long]$endInteger [long]$addressInteger [string]$ipBinary [string]$smBinary [string]$broadcastBinary [string]$networkIdbinary [string]$cidrLookup ipAssist([string]$cidr) { $this.cidrLookup = $cidr $this.getNetworkDetails($cidr) } hidden [void] getNetworkDetails($cidr) { write-verbose 'Getting Network Details' $cidrSplit = $cidr.Split('/') $ipAddressBase =$cidrSplit[0] $cidrInt = [convert]::toInt32($cidrSplit[1]) if($cidrInt -gt 32 -or $cidrInt -lt 0) { throw 'CIDR Invalid' } write-verbose "Using cidr: $cidrInt and ipBase: $ipAddressBase" $this.ipBinary = $this.getBinary($ipAddressBase) $this.smBinary = $this.getCidrBinary($cidrInt) $this.netBits = $this.smBinary.indexOf('0') write-verbose "Netbit: $($this.netbits)" if(($this.netBits -gt 1) -and ($this.netbits -lt 32)) { write-verbose 'Working out network values for multiple range' $this.firstIpAddress = $this.getDottedDecimal($($this.ipBinary.substring(0,$this.netBits).padRight(31,'0')+0)) $this.lastIpAddress = $this.getDottedDecimal($($this.ipBinary.substring(0,$this.netBits).padRight(31,'1')+1)) $this.networkIdbinary = $this.ipBinary.Substring('0',$this.netBits).padRight(32,'0') $this.broadcastBinary = $this.ipBinary.Substring('0',$this.netBits).padRight(32,'1') $this.networkId = $this.getDottedDecimal($this.networkIdbinary) $this.cidr = "$($this.networkId)/$($this.netBits)" $this.startInteger = $this.getIpInteger($this.firstIpAddress) $this.endInteger = $this.getIpInteger($this.lastIpAddress) $this.hostsPerNet = $($this.endInteger - $this.startInteger)+1 }else{ write-verbose 'Working out network values for single ip' $this.firstIpAddress = $this.getDottedDecimal($this.ipBinary) $this.lastIpAddress = $this.firstIpAddress $this.startInteger = $this.getIpInteger($this.firstIpAddress) $this.endInteger = $this.startInteger $this.hostsPerNet = 1 $this.cidr = $this.cidrLookup } write-verbose 'Getting actual CIDR and addressInt' $this.addressInteger = $([system.convert]::ToInt64("$($this.ipBinary)",2)) $this.subnetMask = $this.getDottedDecimal($this.smBinary) } hidden [long] getIpInteger($ipAddress) { write-verbose "Getting IPInt for $ipAddress" $split = $ipAddress.split(".") write-verbose "Split1: $($split[0])" #write-host $split[0] $1 = $([int]$($split[0]) * 16777216) #[math]::pow(256,3) write-verbose "1: $1" $2 = $([int]$($split[1]) * 65536) #[math]::pow(256,2) $3 = $([int]$($split[2]) * 256) $4 = [int]$split[3] return $($1 + $2 + $3 + $4) } hidden [string] getCidrBinary($cidrInt) { write-verbose "Getting cidrBin for $cidrInt" [int[]]$array = (1..32) for($i=0;$i -lt $array.length;$i++) { if($array[$i] -gt $cidrInt) { $array[$i]='0' }else{ $array[$i]=1 } } return $array -join '' } hidden [string] getDottedDecimal($binary) { write-verbose "Getting ipAddress dotNotation for $binary" $i = 0 $dottedDecimal = while($i -le 24) { $convert = [string]$([convert]::toInt32($binary.substring($i,8),2)) $convert $i+= 8 } return $dottedDecimal -join '.' } hidden [string] getBinary($ipAddress) { write-verbose "Getting binary for $ipAddress" $split = $ipAddress.split('.') $parts = foreach($part in $split) { $([convert]::ToString($part,2).padLeft(8,"0")) } return $($parts -join '') } static [long] convertIpToInt($ipAddress) { write-verbose "Getting IPInt for $ipAddress" $split = $ipAddress.split(".") #write-verbose "Split1: $($split[0])" #write-host $split[0] $1 = $([int]$($split[0]) * 16777216) #[math]::pow(256,3) #write-verbose "1: $1" $2 = $([int]$($split[1]) * 65536) #[math]::pow(256,2) $3 = $([int]$($split[2]) * 256) $4 = [int]$split[3] return $($1 + $2 + $3 + $4) } } <#Tests $VerbosePreference = 'silentlycontinue' [ipAssist]::New('10.0.0.0/8') [ipAssist]::New('10.0.0.0/28') [ipAssist]::New('10.0.0.0/32') [ipAssist]::convertIpToInt('192.168.0.99') #> class listener { [int]$port [string]$hostname [object]$httpListener [string]$apiPath [string]$serverPath [bool]$requireToken [int]$numberOfConnections = 0 [int]$defaultCacheTime = 15 [bool]$defaultCacheBehaviour = $true [bool]$defaultAuthBehaviour = $false hidden [string] $configPath hidden [string] $appPath hidden [string] $tokensPath hidden [string] $logPath hidden [bool] $ok hidden [array] $currentTokens hidden [object] $user hidden [hashtable] $requestParams hidden [hashtable] $pageCache = @{} hidden [hashtable] $responseCache = @{} #CONSTRUCTORS DECONS AND INITS #LegacyConstructor #Uses defaults for cacheBehaviour etc #Required to leave it for compatibility reasons listener([int]$port,[string]$hostname,[string]$apiPath,[bool]$requireToken) { $this.verbose('====Listener Initialised: Legacy Constructor===') $this.verbose("Port:$Port;hostname:$hostname;apiPath:$apiPath;requireToken:$requireToken") $this.port = $port $this.hostname = $hostname $this.requireToken = $requireToken $this.serverPath = "http://$($this.hostname):$($this.port)/" $this.apiPath = $apiPath try{ $winOS = [System.Boolean](Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop) }catch{ $winOS = $false } if ($winOS) { $this.verbose('Server is Windows') $this.configPath = "$apiPath\config" $this.appPath = "$apiPath\api" $this.tokensPath = "$($this.configPath)\tokens.xml" $this.logPath = "$($this.configPath)\log.txt" } else { $this.verbose('Server is Not Windows') $this.configPath = "$apiPath/config" $this.appPath = "$apiPath/api" $this.tokensPath = "$($this.configPath)/tokens.xml" $this.logPath = "$($this.configPath)/log.txt" } if(Test-Path $apiPath) { $this.initConfig() $this.verbose('starting listener Config') $this.configListener() $this.listen() }else{ Write-Error 'There is a problem with your api Path' Write-Warning 'This Framework will not work with the current settings' } } #NewConstructor #Allows to set cacheBehaviour etc listener([int]$port,[string]$hostname,[string]$apiPath,[bool]$requireToken,[int]$defaultCacheTime,[bool]$defaultCacheBehaviour,[bool]$defaultAuthBehaviour) { $this.verbose('====Listener Initialised: New Constructor===') $this.verbose("Port:$Port;hostname:$hostname;apiPath:$apiPath;requireToken:$requireToken") $this.port = $port $this.hostname = $hostname $this.requireToken = $requireToken $this.serverPath = "http://$($this.hostname):$($this.port)/" $this.apiPath = $apiPath $this.defaultCacheTime = $defaultCacheTime $this.defaultCacheBehaviour = $defaultCacheBehaviour $this.defaultAuthBehaviour = $defaultAuthBehaviour try{ $winOS = [System.Boolean](Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop) }catch{ $winOS = $false } if ($winOS) { #windows $this.verbose('Server is Windows') $this.configPath = "$apiPath\config" $this.appPath = "$apiPath\api" $this.tokensPath = "$($this.configPath)\tokens.xml" $this.logPath = "$($this.configPath)\log.txt" } else { #Not windows $this.verbose('Server is Not Windows') $this.configPath = "$apiPath/config" $this.appPath = "$apiPath/api" $this.tokensPath = "$($this.configPath)/tokens.xml" $this.logPath = "$($this.configPath)/log.txt" } if(Test-Path $apiPath) { $this.initConfig() $this.verbose('starting listener Config') $this.configListener() $this.listen() }else{ Write-Error 'There is a problem with your api Path' Write-Warning 'This Framework will not work with the current settings' } } [void] initConfig() { $this.verbose('InitConfig') if(!(test-path $this.configPath)) { try{ New-Item -ItemType Directory -path $this.configPath New-Item -ItemType File -Path $this.logPath $this.ok = $true $this.verbose('config folder not found, created') }catch{ Write-Error 'Unable to create configPath' $this.ok = $false } }else{ $this.ok = $true } if(!(test-path $this.appPath)) { $this.verbose('appPath folder not found, creating') try{ New-Item -ItemType Directory -path $this.appPath $this.ok = $true }catch{ Write-Error 'Unable to create appPath' $this.ok = $false } }else{ $this.ok = $true } if(!(Test-path $this.tokensPath)) { #We have no tokens yet #new-item -path $this.tokensPath $this.verbose("No tokens found, making one for the admin") #$newAdmin = new-object adminPage -ArgumentList @('/user/new',@{username='Administrator';isadmin=1},$this.tokensPath,'System') $newAdmin = [adminpage]::new('/user/new',@{username='Administrator';isadmin=1},$this.tokensPath,'System') }else{ $this.verbose("Existing Tokens found, importing") $this.updateTokens() } } [void] configListener() { if($this.httpListener) { #Dispose the existing one #Need to make sure its closed if($this.httpListener.Listening -eq $true) { $this.verbose('Listener still listening, closing') $this.ForceCloseConnection() } } $this.verbose('Creating listener and basic config') try{ $this.verbose('Substantiating listener') #$this.httpListener = $(new-object Net.HttpListener) $this.httpListener = [Net.HttpListener]::new() }catch{ $this.verbose('Unable to substantate listener object') write-error 'Unable to substantiate listener object' } try{ $this.verbose('Configuring listener') $this.httpListener.Prefixes.add($this.serverPath) $this.httpListener.AuthenticationSchemes = 'Anonymous' #$this.httpListener.AuthenticationSchemes = 'Basic' $this.verbose('Listener Config Finished') }catch{ write-error 'Error configuring listener' $this.ForceCloseConnection() } try{ $this.verbose('Starting the listener') $this.httpListener.Start() }Catch{ write-error 'Unable to start listener' $this.ForceCloseConnection() } } [void] closeConnection() { $this.verbose('Resetting connection') $this.user = $null $this.requestParams = @{} } [void] ForceCloseConnection() { $this.verbose('Closing the connection') #Try and clear the params $this.user = $null $this.requestParams = @{} try{ $this.httpListener.stop() $this.httpListener.Close() }catch{ write-warning 'I was unable to stop the listener... HES A MAD-MAN' } } #LISTENER MAIN [void] listen() { #Try and clear the params #Essential to start from clean slate $this.requestParams = @{} $this.user = $null if($this.ok -eq $true) { try{ #$this.verbose('Handling any requests') $this.getRequest() }catch{ write-error 'unable to handle request' $this.ForceCloseConnection() } }else{ $this.ForceCloseConnection() $this.verbose('Listener not started') Write-Warning 'The listener is not started, ok not true' } } #PARAM HELPERS [void] getQueryData($queryString) { $this.verbose('Getting GET Params') $this.requestParams = @{} foreach($get in $queryString) { $this.requestParams."$get" = $queryString[$get] } } [void] getPostData($inputstream,$ContentEncoding) { $this.verbose('Getting POST Params') try{ ##$StreamReader = New-Object IO.StreamReader($inputstream,$ContentEncoding) $this.verbose('Creating Stream Reader') $StreamReader = [IO.StreamReader]::new($inputstream,$ContentEncoding) $this.verbose('Reading Stream') $read = $StreamReader.ReadToEnd() try{ $readJson = $read|ConvertFrom-Json $this.verbose('Json POST data found') $properties = $($readJson | get-member -membertype NoteProperty).Name foreach($property in $properties) { $this.verbose("Creating Property: $property") $this.requestParams."$property" = $readJson."$property" } }catch{ $this.verbose('Fallback to String POST data - using split to extract Params') foreach ($Post in $($read.Split('&'))) { $PostContent = $Post.Split("=") $PostName = $PostContent[0] $PostValue = $($PostContent[1..$($PostContent.count)] -join '=') if($PostName.EndsWith("[]")) { $PostName = $PostName.Substring(0,$PostName.Length-2) } $this.verbose("Creating Property: $PostName") $this.requestParams."$PostName" = $PostValue } } }catch{ $this.verbose('Unable to read stream') } } #REQUEST HELPER #Probably the biggest function [void] getRequest() { $this.verbose('Awaiting Request') $context = $this.httpListener.GetContext() $this.numberOfConnections++ $this.verbose("`n====START====`n`tConnection $($this.numberOfConnections)") $global:lastContectCheck = $context $request = $context.Request $identity = $context.User.Identity $r = $null #write-verbose "`n==`n$($request | format-list * | Out-String)`n==`n" if($request.HttpMethod -eq 'Post') { $this.getPostData($request.InputStream,$request.ContentEncoding) }else{ $this.getQueryData($request.QueryString) } $token = $request.headers['x-api-token'] #$this.verbose("Token Obj : $($token)") $response = $context.Response $Response.Headers.Add('Accept-Encoding','gzip') $Response.Headers.Add('Server','psRapid') #Since this is an API, deal with CORS headers $Response.Headers.Add('Access-Control-Allow-Origin','*') $response.headers.add('Access-Control-Allow-Methods','GET,POST,HEAD,OPTIONS') $this.user = $this.getUser($token) $page = $request.RawUrl.Split('?')[0] $this.verbose("REQUEST DETAILS:`n`tRequested Page: $page`n`tToken: $($token)`n`tRefer: $($request.UrlReferrer)`n`tUserHostAddress: $($request.UserHostAddress)`n`tRemoteEndPoint: $($request.RemoteEndPoint)`n`tIsLocal:$($request.IsLocal)`n") $this.verbose("PARAMS: `n$($this.requestParams|format-list|out-string)`n") #ADMIN PAGE CHECK AND USER AUTH if($page -like '/admin*' -and $this.user.isAdmin -eq $true -and $this.user.enabled -eq $true) { $this.verbose("`n====ADMIN PAGE====`n`Token: $($this.user.token)") try{ $response.StatusCode = '200' #$adminPage = new-object adminPage -ArgumentList @($($page -replace '/admin',''),$this.requestParams,$this.tokensPath,$this.user.token) $adminPage = [adminPage]::New($($page -replace '/admin',''),$this.requestParams,$this.tokensPath,$this.user.token) $r = $adminPage.response.json() if($adminPage.adminCommand -eq 'stop') { $this.verbose('Request to stop server') $this.ok = $false } if($adminPage.adminCommand -eq 'clearCache') { $this.verbose('Request to clear cache') $this.pageCache = @{} $this.responseCache = @{} } if($adminPage.adminCommand -eq 'updateUsers') { $this.verbose('Request to update users') $this.updateTokens() } }catch{ #$this.verbose('Error with Admin Page Creation') write-error 'Error with admin page creation' $adminPage = $null $response.StatusCode = 418 $r = 'Im a Teapot - Error with admin page creation' } #GENERAL PAGE CHECK AND USER AUTH }elseif((($this.user.enabled -eq $true)-and($this.requireToken -eq $true))-or($this.requireToken -eq $false)){ try{ $authorized = $true #$this.verbose('Normal Page Request') $ext = if($page[-1] -eq '/'){''}else{'.ps1'} $requestedPagePath = "$($this.appPath)$($page.replace('/','\'))$($ext)" if(Test-Path $requestedPagePath){ $this.verbose("Path Valid - $requestedPagePath") }else{ $this.verbose("Path invalid, setting to default - $requestedPagePath") $requestedPagePath = $this.appPath } $response.StatusCode = '200' $p = $this.pageCache."$requestedPagePath" if(!$p) { #$this.verbose('Retrieving Page Details') #$p = new-object page -ArgumentList ($requestedPagePath,$page) #The below method works in linux, the above only works in windows, use the below for compatibility reasons $p = [page]::new($requestedPagePath,$page,$this.defaultCacheTime,$this.defaultCacheBehaviour,$this.defaultAuthBehaviour) $this.verbose('Page Retrieved - Saving Page to Cache') $this.pageCache."$requestedPagePath" = $p #$this.verbose("Current Pages Cached: `n`n $($this.pageCache.keys)") }else{ $this.verbose('Page retrieved from Cache') } #This is where we check the user is authorised for the page network access restrictions and if auth is required if($p.ipRanges) { #Whats this users IP Address as a decimal #First need to handle if the request is local, maybe just make it loopback #Then if its not, we need to separate the IP from the Port #Then when we have an IP address, need to convert it to decimal for better maths $authorized = $false $ipAddress = 'a.b.c.d' #Need something because classes are so strict $this.verbose('Need to confirm IP Range') if($request.IsLocal -eq $true) { $ipAddress = '127.0.0.1' }else{ #[::1]:59271 #10.123.42.27:80 $regex = '[^0-9.:]+' $regexIPv4 = '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' $ipAndPortOnly = $request.RemoteEndPoint -replace $regex $ipTest = $($ipAndPortOnly.substring(0,$($ipAndPortOnly.indexOf(':')))).trim() If($ipTest -match $regexIPv4 ){ #IPv4 looks ok $ipAddress = $ipTest }else{ #Not an IP so throw unauth $this.verbose('Not an IPAddress') $authorized = $false } } $this.verbose("Got this IP: $ipAddress") try{ $ipCompare = [ipAssist]::convertIpToInt($ipAddress) $this.verbose("Using IP Int: $ipCompare") foreach($range in $($p.ipRanges)) { if($ipCompare -ge $range.min -and $ipCompare -le $range.max) { $this.verbose("IP in range block, should authorize`n`tRange: $($range.first) <-$ipAddress-> $($range.last)`n`tRange: $($range.min) <-$ipCompare-> $($range.max)") $authorized = $true }else{ $this.verbose("IP not in range block`n`tRange: $($range.first) <-$ipAddress-> $($range.last)`n`tRange: $($range.min) <-$ipCompare-> $($range.max)") } } }catch{ $authorized = $false } }else{ $this.verbose('Not checking IP Range') } if($p.auth -eq $true) { $authorized = $false if($this.user.enabled) { $this.verbose('User is enabled') $authorized = $true } } if($authorized -eq $true) { $this.verbose('Access is authorized') #Ensure script is null $script = $null #Check for headers hashable param in the page inputs $headersInput = $p.inputs | Where-object {$_.name -eq 'headers' -and $_.datatype -eq 'hashtable'} #$this.verbose("$($p.inputs|Format-Table|out-string)") #This allows passing of headers from the request to the scriptblock if required #Should mostly not need this $this.verbose("HeadersInput: `t $(if($headersInput){$true}else{$false})") if($headersInput) { $this.verbose("HeadersFound:`n$($headersInput|Format-Table|out-string)") $headerValuesHash = @{} foreach($item in $context.request.Headers) { $headerValuesHash."$item" = $context.Request.Headers["$item"] } $headerValuesHash.UserHostAddress = $context.request.UserHostAddress $headerValuesHash.UserHostName = $context.request.UserHostName $headerValuesHash.UrlReferrer = $context.request.UrlReferrer $headerValuesHash.IsSecureConnection = $context.request.IsSecureConnection $headerValuesHash.IsLocal = $context.request.IsLocal $headerValuesHash.Cookies = $context.request.Cookies $headerValuesHash.RemoteEndpoint = $context.request.RemoteEndPoint $this.requestParams.Headers = $headerValuesHash #$this.verbose("HeadersPassed: $($headerValuesHash |Format-List|Out-String)") } #$this.verbose('+--=finished param building=--+') #If we have a file, we need to execute it #Check we have a response in the responseCache if($p.cache -eq $true) { $this.verbose('Checking for cached result') #Work out a cacheKey #Should incorporate the page plus the params somehow #$cacheKey = "$($page)::$($($($this.requestParams.keys|sort-object) -join '').tolower())::$($($($this.requestParams.values|sort-object) -join '').tolower())" $cachekeyParams = [array]$($($($this.requestParams.keys)|sort-object)|ForEach-Object{"$($_)$($this.requestParams.$_)"}) -join '' $cacheKey = "$($page)::$cachekeyParams" $this.verbose("Using CacheKey:$cacheKey") if($this.responseCache."$cacheKey") { $this.verbose('responseCache found for object, checking expiry') if($this.responseCache."$cacheKey".expires -gt $(get-date)) { $this.verbose("Cache Valid until: $($this.responseCache."$cacheKey".expires)") $this.verbose('responseCache looks valid, returning cached result') $responsepage = $this.responseCache."$cacheKey".response $responsepage.fromCache() $r = $responsepage.json() }else{ $this.verbose("Cache Expired: $($this.responseCache."$cacheKey".expires)") } } if($r -eq $null) { $this.verbose('No valid cache found. Creating new response') #No response json, make a new response #Add it to the cache as well try{ #$script = new-object script -ArgumentList @($p.filepath,$this.requestParams) if($p.isFile -eq $true) { $script = [script]::new($($p.filepath),$this.requestParams) } $responsepage = [pageResponse]::new($p,$script) $this.responseCache."$cacheKey" = [cacheObject]::New($responsepage,$p.cachetime) $r = $responsepage.json() $this.verbose('Script execution ok') }catch{ $this.verbose('Bad script execution') $r = 'Im a Teapot - Error with page Response' $response.StatusCode = 418 } } }else{ $this.verbose('Cache for page set to false') try{ #$script = new-object script -ArgumentList @($p.filepath,$this.requestParams) if($p.isFile -eq $true) { $script = [script]::new($($p.filepath),$this.requestParams) } $responsepage = [pageResponse]::new($p,$script) $r = $responsepage.json() $this.verbose('Script execution ok') }catch{ $this.verbose('Bad script execution') $r = 'Im a Teapot - Error with page Response' $response.StatusCode = 418 } } }else{ #Return unauthorized $this.verbose('User is unauthorized') $response.StatusCode = '401' $r = 'Access is denied - invalid token' } }catch{ $this.verbose('Error with Page Creation') write-error 'Error with page creation' $response.StatusCode = 418 $r = 'Im a Teapot - Error with page creation' } #ACCESS DENIED }else{ $this.verbose('Creating a deny response') $response.StatusCode = '401' $r = 'Access is denied - invalid token' } $this.user = $null #$this.verbose('Encoding Response') #$this.verbose("RETURN OBJECT: `n$r`n`n") if($r) { $buffer = [System.Text.Encoding]::UTF8.GetBytes($r) $response.ContentLength64 = $buffer.Length }else{ $buffer='' $response.ContentLength64 = 0 } $this.verbose('Sending response') $output = $response.OutputStream $output.Write($buffer,0,$response.ContentLength64) $output.Close() $this.closeConnection() $this.verbose("This connection is finished`n====END====") $this.listen() } #LOGGING HELPER [void] verbose([string]$message) { #Simple verbose helper to include the date $logAs = "[$(get-date -Format s)]`t $message" Write-Verbose $logAs if(!$this.logPath) { write-warning 'Not Logged to file' }else{ try{ $logAs|Out-File $this.logPath -Append -NoClobber -Force $xLogFile = $this.logPath if($xLogFile.length -gt 20mb) { $newname = "$($xLogFile.basename)$(get-date -format yyyyMMdd.hhmmss).txt" rename-item $this.logPath -NewName $newname } }catch{ write-warning 'Error logging to file' } } } #User functions [void] updateTokens() { $this.verbose('Importing tokens') $this.currentTokens = Import-Clixml $this.tokensPath } [object] getUser($token) { $this.verbose("Checking Valid Token: $token") $findUser = $this.currentTokens|Where-Object {$_.token -eq $token} if(!$findUser) { $this.verbose('Token not found, refreshing token list') $this.updateTokens() $findUser = $this.currentTokens|Where-Object {$_.token -eq $token} } if($findUser -and $findUser.enabled -eq $true) { $this.verbose("Token valid") #$this.verbose("`n$($findUser|format-list|out-string)") return $findUser }else{ $this.verbose("Token not valid") return $null } } } class page{ [string]$filepath [hashtable] $links = @{} hidden [bool] $isFile hidden [string] $executePath hidden [string] $pathToReplace hidden [object] $targetFile hidden [int] $cacheTime hidden [bool] $cache hidden [bool] $auth hidden [array] $ipRanges hidden [array] $authGroups [object[]] $inputs page([string] $filepath,[string]$pageRef,[int]$defaultCacheTime,[bool]$defaultCacheBehaviour,[bool]$defaultAuthBehaviour) { $this.filepath = $filepath $this.links.this = $pageRef $this.cacheTime = $defaultCacheTime $this.cache = $defaultCacheBehaviour $this.auth = $defaultAuthBehaviour $endParent = if($pageRef[-1] -eq '/' -and $pageRef.length -gt 1){'/'}else{''} $this.links.parent = "/$($pageref.split('/')[-2])$endParent" try{ $this.targetFile = get-item $filepath -ErrorAction stop if($this.targetFile.PSIsContainer -eq $true) { write-verbose 'Item is directory' $items = get-childitem $filepath -Recurse |where-object {$_.PsIsContainer -eq $true -or $_.Extension -eq '.ps1'} $this.pathToReplace = $this.targetFile.fullname if($this.pathToReplace[-1] -eq '\') { $this.pathToReplace = $this.pathToReplace.Substring(0,$($this.pathToReplace.Length - 1)) } $this.isFile = $false $this.getChildLinks($items) }elseif($this.targetFile.Extension -eq '.ps1'){ write-verbose 'Item is file' $items = get-childItem $this.targetFile.Directory.FullName -Recurse | where-object {$_.PsIsContainer -eq $true -or $_.Extension -eq '.ps1' -and $_.FullName -ne $this.targetFile.fullname} $this.isFile = $true $this.pathToReplace = "$($this.targetFile.Directory.FullName)" $this.executePath = $this.targetFile.FullName $this.getChildLinks($items) $this.getInputs() $this.getAttribs() }else{ Write-Warning 'Incorrect File Type' } }catch{ write-warning 'unable to get the filepath' } } [void] getChildLinks($items) { write-verbose 'Get Child Links' Write-Verbose "Path to Replace: $($this.PathToReplace)" Write-Verbose "ParentPath: $($this.links.parent)" $children = foreach($item in $items) { if($item.PsIsContainer -eq $true) { $base = $item.fullname.replace($($this.PathToReplace),'') $basepath = "$($this.links.parent)$($base.replace('\','/'))/" if($basepath.substring(0,2) -eq '//') { $basepath = $basepath.Substring(1) } write-verbose $basepath $basepath }else{ $base = $item.Directory.fullname.replace($($this.PathToReplace),'') $base = $base.replace('\','/') Write-Verbose $base $basepath = "$($this.links.parent)$($base)/$($item.basename)" if($basepath.substring(0,2) -eq '//') { $basepath = $basepath.Substring(1) } write-verbose $basepath $basepath } } Write-Verbose $($children | out-string) $this.links.children = $children } [void] getInputs() { write-verbose 'Get Inputs' try{ $paramsBase = get-help $this.executePath -Parameter * -ErrorAction Stop foreach($param in $paramsBase) { $this.inputs += [pscustomobject] @{ name = $param.name description = $param.description.text datatype = $param.type.name } } }catch{ write-warning 'No Declared parameters' } } [void] getAttribs() { write-verbose 'Get Attribs' try{ $command = get-command $this.executePath if($command) { $attribData = $command.ScriptBlock.Attributes|where-object{$_.typeid.name -eq 'PageControl'} if($attribData) { write-verbose 'Got attrib data, adding to page details' if($attribData.cacheMins) { write-verbose "Setting Cache Mins to: $($attribData.cacheMins)" $this.cacheTime = $attribData.cacheMins } if($attribData.cache -ne $null) { write-verbose "Setting Cache : $($attribData.cacheMins)" $this.cache = $attribData.cache } if($attribData.tokenRequired -ne $null) { write-verbose "Setting auth : $($attribData.tokenRequired)" $this.auth = $attribData.tokenRequired } if($attribData.networkRange) { $this.ipRanges = forEach($network in $attribData.networkRange) { write-verbose "Adding Netrange for : $network" $netData = [ipAssist]::new($network) @{ first = $netData.firstIpAddress last = $netData.lastIpAddress min = $netData.startInteger max = $netData.endInteger } } } if($attribData.authGroup) { $this.authGroups = $attribData.authGroup } }else{ write-warning 'No attrib data found' } }else{ write-warning 'Unable to get command data' } }catch{ write-warning 'Unable to import command' } } } #Should we cache #If so for how long should we cache #Should we hide the link #Should we restrict to CIDR #Need a CIDR Helper #Should be an attribute #should cacheControl be separate from the functionControls? #Needs more thought #Auth Groups #Should be in the page class class PageControl : System.Attribute { [int] $cacheMins [bool] $cache [array] $networkRange [array] $authGroup [bool] $tokenRequired PageControl() { } } class response{ [hashtable] $links [object[]] $inputs [int] $itemCount [object[]] $items [string]$server = 'psRapid' [string]$timestamp = $(get-date -format s) [bool]$cachedResponse = $false respones() { $objType = $this.GetType() if($objType -eq [response]) { throw "Parent Class $($objType.Name) Must be inherited" } } [string] json() { return $this|ConvertTo-Json -depth 10 } [void] fromCache() { $this.timestamp = $(get-date -format s) $this.cachedResponse = $true } } class pageResponse : response { pageResponse([object]$page,[object]$script) { $this.links = $page.links $this.inputs = $page.inputs if($script.results) { $this.itemCount = $($script.results |measure-object).count $this.items = $script.results }else{ $this.itemCount = 0 $this.items = $null } } } class adminResponse : response { adminResponse($items,$links,$inputs) { $this.links = $links $this.items = $items $this.itemcount = $($items |measure-object).count $this.inputs = $inputs } } class accessDeniedResponse : response { } class script { [object[]]$results [hashtable]$params [string]$filepath script([string]$filepath,[hashtable]$params) { $this.filepath = $filepath $this.params = $params try{ $file = get-item $this.filepath -ErrorAction stop }catch{ Write-warning 'File not Found' $file = $null } if(($file -and $file.Extension -eq '.ps1')) { $this.execute() }else{ write-warning 'Invalid FileType' } } [void] execute() { if($this.params.count -gt 0) { write-verbose 'Executing with params' $splat = $this.params $scriptResult = . $this.filepath @splat }else{ write-verbose 'Executing wihtout params' $scriptResult = . $this.filepath } if($scriptResult) { Write-Verbose 'Saving Results' $this.results = $scriptResult }else{ Write-Verbose 'No Results returned' } } } |