Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
feedthedogs committed Aug 19, 2018
1 parent 23213bf commit 4b8a926
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################

/.vs
/PBIProxy/bin/Debug/netcoreapp2.1
/PBIProxy/obj
/PBIProxy/PBICert.pfx
/PBIProxy/PBIProxy.csproj.user
/PBIProxy/PBIStaticFiles/attribution/Vector
/PBIProxy/PBIStaticFiles/powerbi/1.0.0.82
/PBIProxy/PBIStaticFiles/sharing/rest/content/items
/PBIProxy/PBIStaticFiles/sharing/rest/portals
/PBIProxy/bin/Release/netcoreapp2.1
25 changes: 25 additions & 0 deletions PBIProxy.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2042
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PBIProxy", "PBIProxy\PBIProxy.csproj", "{8FBDC86D-4A38-4DF4-B860-FBEEE634A87C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8FBDC86D-4A38-4DF4-B860-FBEEE634A87C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FBDC86D-4A38-4DF4-B860-FBEEE634A87C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FBDC86D-4A38-4DF4-B860-FBEEE634A87C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FBDC86D-4A38-4DF4-B860-FBEEE634A87C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B836C1AB-F5BB-4EDE-A7D7-00204608D775}
EndGlobalSection
EndGlobal
20 changes: 20 additions & 0 deletions PBIProxy/PBIProxy.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TieredCompilation>true</TieredCompilation>
<StartupObject></StartupObject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
41 changes: 41 additions & 0 deletions PBIProxy/PBIStaticFiles/PrepareFilesAndCertificate.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
###### Create a new Certificate to impersonate ArcGIS' domains

# This is for testing purposes, you should use a CA to sign a certificate and keep it in the Certificate Manager
# Create a selfsigned certficiate with the list of domains
$cert = New-SelfSignedCertificate -FriendlyName "PBIProxy" -KeyLength 2048 -certstorelocation Cert:\LocalMachine\My -DnsName "lacdn.arcgis.com", "www.arcgis.com","basemaps.arcgis.com","utility.arcgis.com","static.arcgis.com"
# Move it to be trusted as a Root CA
Move-Item (Join-Path Cert:\LocalMachine\My $cert.Thumbprint) -Destination Cert:\LocalMachine\Root

# Export the certificate so that the webserver can use it
$mypwd = ConvertTo-SecureString -String "Password01$" -Force -AsPlainText
Get-ChildItem -Path (Join-Path Cert:\LocalMachine\Root $cert.Thumbprint) | Export-PfxCertificate -FilePath ..\PBICert.pfx -Password $mypwd


###### Download Static PowerBI ArcGIS files

# Have the requests look similar to PowerBI
$userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.122 Safari/537.36'
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add('Accept','*/*')
$headers.Add('Referer','https://app.powerbi.com/')
$headers.Add('Origin','ms-pbi://pbi.microsoft.com')

# Get the list of Static files that are not provided by ArcGis offline
$root = (Get-Item -Path ".\").FullName
$urls = Get-Content "$root\Urls.txt"
foreach($url in $urls) {
# Build the directories to match the paths that PowerBI requests
$path = $url.Split('/')
$buildpath = '\'
for ($i = 3; $i -lt $path.Count - 1; $i++) {
# Write-Output "$root$($buildpath)$($path[$i])"
md -Path "$root$($buildpath)$($path[$i])" -ErrorAction SilentlyContinue
$buildpath += "$($path[$i])\"
}
# Querystrings are appended to the end of the filename with an underscore
$filename = $path[$path.Count - 1].Replace('?','_')

# Get the file and write it to a folder to serve from
Write-Output "Downloading: $root$($buildpath)$filename"
Invoke-WebRequest $url -OutFile "$root$($buildpath)$filename" -UserAgent $userAgent -Headers $headers
}
32 changes: 32 additions & 0 deletions PBIProxy/PBIStaticFiles/urls.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/emf.css
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/LoadingIndicator_Gold.gif
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/esriScience.svg
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/popup_sprite.png
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/sprite.svg
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/plususer.svg
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/Caret_12.png
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/common/images/getplus.svg
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/fonts/StandardFont.woff
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/themes/fonts/SegoeUI-Bold-final.woff
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/main.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/nls/main_en-us.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/adapters/config/arcgisAdapterSettingsProd.json
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/manifest.json.txt
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriPBI/widgets/ConfirmDialog/css/ConfirmDialog.css
https://lacdn.arcgis.com/powerbi/1.0.0.82/dojo/resources/blank.gif
https://lacdn.arcgis.com/powerbi/1.0.0.82/dojo/dojo.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriTelemetry/telemetry.dojo.min.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esriTelemetry/telemetry.dojo.min.js.map
https://lacdn.arcgis.com/powerbi/1.0.0.82/xstyle/core/load-css.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esri/images/map/logo-med.png
https://lacdn.arcgis.com/powerbi/1.0.0.82/esri/layers/vectorTiles/core/workers/worker.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esri/layers/vectorTiles/core/workers/worker-init.js
https://lacdn.arcgis.com/powerbi/1.0.0.82/esri/layers/vectorTiles/core/workers/nls/worker-init_en-us.js
https://www.arcgis.com/sharing/rest/portals/self?culture=en-us&f=json
https://www.arcgis.com/sharing/rest/content/items/291da5eab3a0412593b66d384379f89f/resources/styles/root.json?appID=mapsForPowerBI&f=json
https://www.arcgis.com/sharing/rest/content/items/291da5eab3a0412593b66d384379f89f/resources/sprites/sprite.json?appID=mapsForPowerBI
https://www.arcgis.com/sharing/rest/content/items/291da5eab3a0412593b66d384379f89f/resources/sprites/sprite.png
https://www.arcgis.com/sharing/rest/content/items/1768e8369a214dfab4e2167d5c5f2454/resources/styles/root.json?appID=mapsForPowerBI&f=json
https://www.arcgis.com/sharing/rest/content/items/1768e8369a214dfab4e2167d5c5f2454/resources/sprites/sprite.json?appID=mapsForPowerBI
https://www.arcgis.com/sharing/rest/content/items/1768e8369a214dfab4e2167d5c5f2454/resources/sprites/sprite.png
https://static.arcgis.com/attribution/Vector/World_Basemap_v2?f=json
17 changes: 17 additions & 0 deletions PBIProxy/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace PBIProxy
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
19 changes: 19 additions & 0 deletions PBIProxy/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:443",
"sslPort": 443
}
},
"profiles": {
"PBIProxy": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
129 changes: 129 additions & 0 deletions PBIProxy/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;

namespace PBIProxy
{
public class Startup
{
static HttpClient _httpClient { get; set; }
static string _geoCodeServerURL { get; set; }
static string _tileServerURL { get; set; }
static string _pbiTileServerURL { get; set; }

public Startup(IConfiguration configuration)
{
_tileServerURL = configuration["TileServerURL"];
_pbiTileServerURL = configuration["PBITileServerURL"];
_geoCodeServerURL = configuration["GeoCodeServerURL"];

var proxy = new WebProxy()
{
Address = new Uri("http://192.168.2.7:8888")
};
var httpClientHandler = new HttpClientHandler()
{
Proxy = proxy,
};
_httpClient = new HttpClient(httpClientHandler, false);
}

public void ConfigureServices(IServiceCollection services)
{
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.Map("/usrsvcs", HandleGeoCodeServer);
app.Map("/arcgis", HandleTileMapServer);

app.Use(async (context, next) =>
{
// Query strings have been converted to static files trailing with an underscore and the filename
if (context.Request.QueryString.Value != "")
{
context.Request.Path = string.Format("{0}_{1}", context.Request.Path.Value, context.Request.QueryString.Value.Substring(1));
}
await next();
});

// for each of the external folders make them accessible in the root folder
var externalFolder = Path.Combine(Directory.GetCurrentDirectory(), "PBIStaticFiles");
foreach (var folder in Directory.EnumerateDirectories(externalFolder))
{
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx => {
ctx.Context.Response.Headers.Append("Access-Control-Allow-Origin", "ms-pbi://pbi.microsoft.com");
},
FileProvider = new PhysicalFileProvider(folder),
RequestPath = string.Format("/{0}", new DirectoryInfo(folder).Name),
ServeUnknownFileTypes = true,
DefaultContentType = "application/json"
});
}
}

private static void HandleGeoCodeServer(IApplicationBuilder app)
{
app.Run(async context =>
{
// Translate from the Request.From to a KeyValuePair collection
var formData = new List<KeyValuePair<string, string>>();
foreach (var k in context.Request.Form.Keys)
{
formData.Add(new KeyValuePair<string, string>(k, context.Request.Form[k]));
}

// fetch the response from the configured source
var postContent = new FormUrlEncodedContent(formData);
var response = await _httpClient.PostAsync(_geoCodeServerURL, postContent);

string content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
context.Response.Headers.Add("Access-Control-Allow-Origin", "ms-pbi://pbi.microsoft.com");
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
await context.Response.WriteAsync(content);
}
else
{
await context.Response.WriteAsync("An error occurred: " + content);
}
});
}

private static void HandleTileMapServer(IApplicationBuilder app)
{
app.Run(async context =>
{
// fetch the response from the configured source
var ExtraURL = context.Request.Path.Value.Replace(_pbiTileServerURL, "");
var requestURL = $"{_tileServerURL}{ExtraURL}{context.Request.QueryString.Value}";
var response = await _httpClient.GetAsync(requestURL);

string content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
context.Response.Headers.Add("Access-Control-Allow-Origin", "ms-pbi://pbi.microsoft.com");
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
context.Response.Headers.Add("Cache-Control", response.Headers.CacheControl.ToString());
context.Response.Headers.Add("Last-Modified", response.Content.Headers.LastModified.ToString());
await context.Response.WriteAsync(content);
}
else
{
await context.Response.WriteAsync("An error occurred: " + content);
}
});
}
}
}
9 changes: 9 additions & 0 deletions PBIProxy/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
23 changes: 23 additions & 0 deletions PBIProxy/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"GeoCodeServerURL": "https://utility.arcgis.com/usrsvcs/appservices/L7zl5sI0W1hBHWRI/rest/services/World/GeocodeServer/geocodeAddresses?appID=mapsForPowerBI",
"TileServerURL": "https://basemaps.arcgis.com/arcgis/rest/services/World_Basemap_v2/VectorTileServer",
// Don't change unless PowerBI changes its url:
"PBITileServerURL": "/rest/services/World_Basemap_v2/VectorTileServer",
"AllowedHosts": "*",
"Kestrel": {
"EndPoints": {
"Https": {
"Url": "https://*:443",
"Certificate": {
"Path": "PBICert.pfx",
"Password": "Password01$"
}
}
}
}
}
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
# PBIArcGISMapProxy
Proxies PowerBI maps to an onprem ArcGIS instance
Proxies PowerBI maps to an onprem or offline ArcGIS instance

Required on the Server:

1. Install .NET Core 2.1
https://www.microsoft.com/net/download/dotnet-core/2.1

2. Build a trusted Certificate for arcgis.com etc and download static PowerBI ArcGIS files to be served to the clients:
Open the powershell file PBIStaticFiles\PrepareFilesAndCertificate.ps1
and run it manually so you understand what it is doing and adjust where appropiate for you

Required on the Client:

1. Start the PowerBI client online once so it can download the ArgGIS icon and resources
The files can then be copied offline from C:\Users\<user>\AppData\Local\Microsoft\Power BI Desktop\CEF

2. Add the domains to the hosts file (127.0.0.1 is local system or where the proxy is to be hosted):
C:\Windows\System32\drivers\etc\hosts (open as administrator)
127.0.0.1 lacdn.arcgis.com
127.0.0.1 www.arcgis.com
127.0.0.1 basemaps.arcgis.com
127.0.0.1 utility.arcgis.com
127.0.0.1 static.arcgis.com

0 comments on commit 4b8a926

Please sign in to comment.