Consuming REST service with SSO authentication - javascript

There is a server from which I consume web services (https://example.com/zzzzzz/rest/services/)
If I just paste this in Chrome, I'm prompted to authenticate. I put my known credentials and them I'm free to roam around.
However, if I try to access something like :
https://example.com/zzzzzz/rest/services/tttttt/uuuuuu/MapServer
in Javascript with XMLHttpRequest, I get a 401 Unauthorized response every time. It works if I add a header to this request with my credentials:
xml_req.setRequestHeader("Authorization", "Basic " + btoa("username:password");
However, this would mean to expose that username and password in every JS code and also add a header to each XMLHttpRequest (which I cannot do at this point).
The above server is not mine, so I cannot do anything to it other than consume services after I login.
Is there a way I can get my own server (IIS) to handle this authentication for me whenever I try to access those services?
Extra info : This is all for an ArcGIS server.

I found the solution.
The idea is to create a webservice (ashx in my case) which gets the requested service to be proxied as a parameter (myproxy.com?https://websitetobeproxied.com/serviceToBeProxied), sends it to the proxied server along with the credentials (using network credentials), fetches the result and sends it back to the client.
This would be the http request function which passes the credentials :
private System.Net.WebResponse doHTTPRequest(string uri, byte[] bytes, string method, string contentType, ServerUrl serverUrl) {
System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(uri);
req.ServicePoint.Expect100Continue = false;
// Add credentials
req.Credentials = new NetworkCredential("username", "password");
//For testing I automatically validate any ssl certificates. You may want to disable this in production
req.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => {
return true;
};
//Get the request method ("GET", "SET" etc.)
req.Method = method;
if (bytes != null && bytes.Length > 0 || method == "POST") {
req.Method = "POST";
req.ContentType = string.IsNullOrEmpty(contentType) ? "application/x-www-form-urlencoded" : contentType;
if (bytes != null && bytes.Length > 0)
req.ContentLength = bytes.Length;
using (Stream outputStream = req.GetRequestStream()) {
outputStream.Write(bytes, 0, bytes.Length);
}
}
return req.GetResponse();
}
And this would be the method to fetch and send the result back :
private bool fetchAndPassBackToClient(System.Net.WebResponse serverResponse, HttpResponse clientResponse, bool ignoreAuthenticationErrors)
{
if (serverResponse != null)
{
clientResponse.ContentType = serverResponse.ContentType;
using (Stream byteStream = serverResponse.GetResponseStream())
{
// Text
if (serverResponse.ContentType.Contains("text") ||
serverResponse.ContentType.Contains("json") ||
serverResponse.ContentType.Contains("xml"))
{
using (StreamReader sr = new StreamReader(byteStream))
{
string strResponse = sr.ReadToEnd();
if (
!ignoreAuthenticationErrors
&& strResponse.IndexOf("{\"error\":{") > -1
&& (strResponse.IndexOf("\"code\":498") > -1 || strResponse.IndexOf("\"code\":499") > -1)
)
return true;
clientResponse.Write(strResponse);
}
}
else
{
// Binary response (Image or other binary)
// Don't cache the result
clientResponse.CacheControl = "no-cache";
byte[] buffer = new byte[32768];
int read;
while ((read = byteStream.Read(buffer, 0, buffer.Length)) > 0)
{
clientResponse.OutputStream.Write(buffer, 0, read);
}
clientResponse.OutputStream.Close();
}
serverResponse.Close();
}
}
return false;
}

Related

Client receive different http code from what the server had response

I had this weird going on.
I'm using Jersey for API end-point, HTML and javascript front end. When I authenticate user, server will response 303 to tell the client the user is authenticated and to redirect to the said page (based on user group which the user belongs to), which I put it in Location header.
But when my server response with code 303, the client (html with pure javascript in this case) receive code of 200. But if I change the code to something else, let say 401, the client receive it correctly. This happened only if server response with code 303.
Why I return 303? I'm not pretty sure myself, thought it is the right way to do it, I might just return 200, but I try to do it the proper way as much as I know and can. But that is for another time, suggestion are welcome.
And even when I try to receive the Location from header and token from cookies, it return null. As if something happened to the response from server to client. I don't have anything that change response.
Looking at the application log, everything went fine, nothing miss behave.
It was working fine, but suddenly this weird behavior happened. I already clear browser cache, clean build and deploy the app, restart tomcat, and even restart my dev machine, nothing solve it.
I not found anything related with google. My way of code things might not the right way as I'm quite new with REST.
My javascript:
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if(xhr.status === 200) {
console.log("Login API return 200");
console.log("Token: " + xhr.getResponseHeader("Token"));
} else if (xhr.status === 303) {
console.log("Status 303 with response text " + xhr.responseText);
console.log("Status 303 with redirect header of " + xhr.getResponseHeader("redirect"));
// redirect user to the page
window.location.href = xhr.getResponseHeader("Location");
} else if (xhr.status === 401) {
console.log('Unauthorized access to server.');
document.getElementById("expired").style.display = "none";
document.getElementById("credential").style.display = "block";
} else {
console.log("Returned status is " + xhr.status);
console.log("Response text is " + xhr.responseText);
}
}
};
xhr.open('POST', URL + "/api/v1/users/login");
xhr.setRequestHeader("authorization", authHeader);
xhr.send();
My java:
#POST
#Path("/login")
#Produces(MediaType.TEXT_PLAIN)
public Response authUser(#Context HttpHeaders httpHeaders,
#Context HttpServletRequest request) {
log.debug("Server name : " + request.getServerName());
String authHeader = httpHeaders.getRequestHeader("authorization").get(0);
String encodeAuth = authHeader.substring(authHeader.indexOf(' ') + 1);
String decodeAuth = new String(Base64.getDecoder().decode(encodeAuth));
String username = decodeAuth.substring(0, decodeAuth.indexOf(':'));
String password = decodeAuth.substring(decodeAuth.indexOf(':') + 1);
User user = new User(username);
log.debug("Username : " + username);
log.debug("Password : " + password);
// Check user
if (!user.login(username,password)) {
// This is a response for the ajax for unauthorized login
log.warn("Can not authenticate user, either wrong username or password.");
// Return Unauthorized response
return Response
.status(401)
.entity("Unauthorized")
.build();
}
log.debug("Authentication success. Proceed with token and cookie creation");
// Create token
String token = TokenStore.getInstance().putToken(username);
// Get user's group
String userGroup = TokenStore.getInstance().getGroup(token);
log.debug("Token created for the user is " + token);
// Create cookie
NewCookie cookie = new NewCookie("Token",token,"/app/", request.getServerName(),"User token",1800,false);
/* Create redirect URL.
* uriInfo.getBaseUriBuilder() will return http://<host>:<port>/app/api/ which is not
* desired in this scenario, the below is to eliminate /api/ from the URI
* to get only http://<host>:<port>/app
*/
String uri = uriInfo.getBaseUriBuilder().toString();
// nthLastIndexOf(2, "/", uri) is a helper method to get the nth occurrence of a character in a string, from the last character.
int secondLast = nthLastIndexOf(2, "/", uri);
String base = uri.substring(0,secondLast);
String redirect = "";
if(null != userGroup) switch (userGroup) {
case "groupA":
redirect = base + "/pageA";
break;
case "groupB":
redirect = base + "/pageB";
break;
case "groupC":
redirect = base + "/pageC";
break;
default:
break;
}
URI re = null;
log.debug("Created URI for redirect is " + redirect);
try {
re = new URI(redirect);
} catch (URISyntaxException syntax) {
log.error("Cannot convert string to URI", syntax);
} catch (Exception e) {
log.error("Cannot convert string to URI", e);
}
log.debug("Return response 303.");
// Return redirect response
return Response
.status(303)
.entity(token)
.header("Location", redirect)
.cookie(cookie)
.build();
}

javascript - Intercept and modify XHR response in chrome plugin

I'm writing a plugin that will intercept all the requests and responses and will extract data and if needed also modify the response. Below is the code I'm using to intercept the request, but it seems I can only read the response and not modify it. The code is injected into the page by manifest.json.
(function(xhr)
{
var XHR = XMLHttpRequest.prototype;
var send = XHR.send;
XHR.send = function(postData)
{
this.addEventListener('load', function()
{
if (postData)
{
if (typeof postData === 'string')
{
try
{
this._requestHeaders = postData;
} catch (err)
{
console.log(err);
}
}
else if (typeof postData === 'object' || typeof postData === 'array' || typeof postData === 'number' || typeof postData === 'boolean')
{
var enc = new TextDecoder("utf-8");
requestdata = enc.decode(postData);
console.log("postData");
var json = JSON.parse(requestdata);
console.log(json);
// Extract data from request
var req = this.responseText
// Change req, this does nothing!
this.responseText = req;
}
}
});
return send.apply(this, arguments);
};
})(XMLHttpRequest);
I understand this is because responseText is actually read only, and then property itself returns a cloned string, rather than a reference to actual response text. Is there any way around it? The only other way I see is using CEF, opening a web site from my CEF application and intercepting the requests there, which is nice, I can enhance the web site inside my application, but on the other hand it's cumbersome and I want my users to be able to download the plugin instead of having to use an exe.
Thanks!

Why does getting Django's CSRF token from a cookie fail on mobile Chrome browser?

A web application in Django with React components currently has been tested and works on desktop Google Chrome, Microsoft Edge, mobile Firefox and mobile Brave browsers. Unfortunately, it produces errors on Google Chrome on mobile. The React components do not seem to recognize that there is a user logged in.
The CSRF token is transferred from Django to the React components using a cookie (similarly to the process suggested at: https://django.readthedocs.io/en/stable/ref/csrf.html).
// Set what happens when the database is called by React
// Define how to get a cookie (such as a CSRF token)
export function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie !== '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Define generically what happens when the database is called by React
export function backendLookup(method, endpoint, callback, data) {
// Convert the data being sent to the database to JSON form
let jsonData;
if (data){
jsonData = JSON.stringify(data)
}
// Prepare a new request to a database
const xhr = new XMLHttpRequest()
// Prepare the URL where the database is accessible (the API endpoint)
const url = `https://artformist.com/api${endpoint}`
// Identify that the data that will be receievd is in JSON format
xhr.responseType = "json"
// Prepare the CSRF security token
const csrftoken = getCookie('csrftoken');
// Access the database, using the endpoint and method (such as "POST" or "GET") in use
xhr.open(method, url)
// Set the request header, for security purposes
xhr.setRequestHeader("Content-Type", "application/json")
// If there is a CSRF token...
if (csrftoken){
// Set the request headers
// xhr.setRequestHeader("HTTP_X_REQUESTED_WITH", "XMLHttpRequest")
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
// Set what happens immediately when a request is made
xhr.onload = function() {
// If the status returned is a 403 error...
if (xhr.status === 403) {
// Get the details of the response...
const detail = xhr.response.detail
// If the response is that the user wasn't logged in...
if (detail === "Authentication credentials were not provided."){
if (window.location.href.indexOf("login") === -1) {
// Redirect the user to the login screen TURNED OFF FOR TESTING
// window.location.href = "/login?showLoginRequired=true&status=403"
}
alert(xhr.status)
}
}
// // If the response is that something has gone wrong with the server
if (xhr.status === 500) {
// If the user isn't at a login screen already...
if (window.location.href.indexOf("login") === -1) {
// Redirect the user to the login screen. TURNED OFF FOR TESTING
// window.location.href = "/login?showLoginRequired=true&status=500"
alert(xhr.status)
}
}
// Manage the request response and status
callback(xhr.response, xhr.status)
}
// If there is an error when the call to the database is made
xhr.onerror = function (e) {
// Manage that there was a 400 error
callback({"message": "The request was an error"}, 400)
}
// Send the JSON data
xhr.send(jsonData)
}
On Chrome mobile, the site has been producing 403 or 500 error depending on what the component is attempting to do.
Here are some of the production settings. The SameSite settings have been changed and the Django version has been upgraded.
CORS_REPLACE_HTTPS_REFERER = True
HOST_SCHEME = "https://"
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Set whether requests can be made from different sites
CSRF_COOKIE_SAMESITE = None
SESSION_COOKIE_SAMESITE = None
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_SECONDS = 1000000
SECURE_FRAME_DENY = True

How to enable CORS in an Azure App Registration when used in an OAuth Authorization Flow with PKCE?

I have a pure Javascript app which attempts to get an access token from Azure using OAuth Authorization Flow with PKCE.
The app is not hosted in Azure. I only use Azure as an OAuth Authorization Server.
//Based on: https://developer.okta.com/blog/2019/05/01/is-the-oauth-implicit-flow-dead
var config = {
client_id: "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx",
redirect_uri: "http://localhost:8080/",
authorization_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize",
token_endpoint: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
requested_scopes: "openid api://{tenant-id}/user_impersonation"
};
// PKCE HELPER FUNCTIONS
// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
const hashed = await sha256(v);
return base64urlencode(hashed);
}
// Parse a query string into an object
function parseQueryString(string) {
if (string == "") { return {}; }
var segments = string.split("&").map(s => s.split("="));
var queryString = {};
segments.forEach(s => queryString[s[0]] = s[1]);
return queryString;
}
// Make a POST request and parse the response as JSON
function sendPostRequest(url, params, success, error) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.onload = function () {
var body = {};
try {
body = JSON.parse(request.response);
} catch (e) { }
if (request.status == 200) {
success(request, body);
} else {
error(request, body);
}
}
request.onerror = function () {
error(request, {});
}
var body = Object.keys(params).map(key => key + '=' + params[key]).join('&');
request.send(body);
}
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = 'Hello'+ 'webpack';
element.classList.add('hello');
return element;
}
(async function () {
document.body.appendChild(component());
const isAuthenticating = JSON.parse(window.localStorage.getItem('IsAuthenticating'));
console.log('init -> isAuthenticating', isAuthenticating);
if (!isAuthenticating) {
window.localStorage.setItem('IsAuthenticating', JSON.stringify(true));
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("pkce_state", state);
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);
// Build the authorization URL
var url = config.authorization_endpoint
+ "?response_type=code"
+ "&client_id=" + encodeURIComponent(config.client_id)
+ "&state=" + encodeURIComponent(state)
+ "&scope=" + encodeURIComponent(config.requested_scopes)
+ "&redirect_uri=" + encodeURIComponent(config.redirect_uri)
+ "&code_challenge=" + encodeURIComponent(code_challenge)
+ "&code_challenge_method=S256"
;
// Redirect to the authorization server
window.location = url;
} else {
// Handle the redirect back from the authorization server and
// get an access token from the token endpoint
var q = parseQueryString(window.location.search.substring(1));
console.log('queryString', q);
// Check if the server returned an error string
if (q.error) {
alert("Error returned from authorization server: " + q.error);
document.getElementById("error_details").innerText = q.error + "\n\n" + q.error_description;
document.getElementById("error").classList = "";
}
// If the server returned an authorization code, attempt to exchange it for an access token
if (q.code) {
// Verify state matches what we set at the beginning
if (localStorage.getItem("pkce_state") != q.state) {
alert("Invalid state");
} else {
// Exchange the authorization code for an access token
// !!!!!!! This POST fails because of CORS policy.
sendPostRequest(config.token_endpoint, {
grant_type: "authorization_code",
code: q.code,
client_id: config.client_id,
redirect_uri: config.redirect_uri,
code_verifier: localStorage.getItem("pkce_code_verifier")
}, function (request, body) {
// Initialize your application now that you have an access token.
// Here we just display it in the browser.
document.getElementById("access_token").innerText = body.access_token;
document.getElementById("start").classList = "hidden";
document.getElementById("token").classList = "";
// Replace the history entry to remove the auth code from the browser address bar
window.history.replaceState({}, null, "/");
}, function (request, error) {
// This could be an error response from the OAuth server, or an error because the
// request failed such as if the OAuth server doesn't allow CORS requests
document.getElementById("error_details").innerText = error.error + "\n\n" + error.error_description;
document.getElementById("error").classList = "";
});
}
// Clean these up since we don't need them anymore
localStorage.removeItem("pkce_state");
localStorage.removeItem("pkce_code_verifier");
}
}
}());
In Azure I only have an App registration (not an app service).
Azure App Registration
The first step to get the authorization code works.
But the POST to get the access token fails. (picture from here)
OAuth Authorization Code Flow with PKCE
Access to XMLHttpRequest at
'https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token' from
origin 'http://localhost:8080' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Where in Azure do I configure the CORS policy for an App Registration?
Okay, after days of banging my head against the stupidity of Azure's implementation I stumbled upon a little hidden nugget of information here: https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser#prerequisites
If you change the type of the redirectUri in the manifest from 'Web' to 'Spa' it gives me back an access token! We're in business!
It breaks the UI in Azure, but so be it.
You should define the internal url with your local host address.
https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/application-proxy-understand-cors-issues
When I first posted, the Azure AD token endpoint did not allow CORS requests from browsers to the token endpoint, but it does now. Some Azure AD peculiarities around scopes and token validation are explained in these posts and code in case useful:
Code Sample
Blog Post

Server Sent Events with AJAX: How to resolve SSE GET with XHR POST?

I'm trying to resolve an issue between, what I perceive is, AJAX and Server Sent Events. I have an application that does a post with some instructions to the controller, and I would like the controller to send some commentary back as an event to let the user know that the action requested has been performed (can have errors or take a while).
The idea is that the user can send a package of different instructions through the client, and the server will report through SSE when each of these actions are completed.
The problem I see through Fiddler is that when the post is performed, the response that it gets back contains my eventsource message that I would like used. However, the eventsource code also appears to call a GET, in which it appears to want that eventsource message. Because it doesn't get that, the connection repeatedly closes.
I currently have some controller code like so:
[System.Web.Http.HttpPost]
public void Stop(ProjectViewModel model)
{
ProjectManager manager = new ProjectManager();
if (model.Servers != null && model.Servers.Count != 0)
{
string machine = model.Servers[0];
foreach (string service in model.Services)
{
manager.StopService(service, machine);
Message("stop", service);
}
}
}
and in my view, both Ajax/XHR and server sent events set up like so:
var form = document.getElementById("submitform");
form.onsubmit = function (e) {
// stop the regular form submission
e.preventDefault();
// collect the form data while iterating over the inputs
var data = {};
for (var i = 0, ii = 2; i < ii; ++i) {
var input = form[i];
if (input.name == "Servers") {
data[input.name] = document.getElementById("ServerSelect").options[document.getElementById("ServerSelect").selectedIndex].text;
}
else if (input.name == "Services")
data[input.name] = document.getElementById("ServiceSelect").options[document.getElementById("ServiceSelect").selectedIndex].text;
}
if (action) { data["action"] = action };
// construct an HTTP request
var xhr = new XMLHttpRequest();
if (action == "stop") {
xhr.open(form.method, '/tools/project/stop', true);
}
if (action == "start") {
xhr.open(form.method, '/tools/project/start', true)
}
xhr.setRequestHeader('Content-Type', 'application/json; charset=urf-8');
// send the collected data as JSON
xhr.send(JSON.stringify(data));
xhr.onloadend = function () {
// done
};
};
function events() {
if (window.EventSource == undefined) {
// If not supported
document.getElementById('eventlog').innerHTML = "Your browser doesn't support Server Sent Events.";
} else {
var source = new EventSource('../tools/project/Stop');
source.addEventListener("message", function (message) { console.log(message.data) });
source.onopen = function (event) {
document.getElementById('eventlog').innerHTML += 'Connection Opened.<br>';
console.log("Open");
};
source.onerror = function (event) {
if (event.eventPhase == EventSource.CLOSED) {
document.getElementById('eventlog').innerHTML += 'Connection Closed.<br>';
console.log("Close");
}
};
source.onmessage = function (event) {
//document.getElementById('eventlog').innerHTML += event.data + '<br>';
var newElement = document.createElement("li");
newElement.textContent = "message: " + event.data;
document.getElementById("eventlog").appendChild(newElement)
console.log("Message");
};
}
};
I'm somewhat new to web development, and I'm not sure how to resolve this issue. Is there a way I can have the eventsource message read from that POST? Or have it sent to the GET instead of being sent as a response to the POST? Overall, it seems that the most damning issue is that I can't seem to get the event messages sent to the GET that is requested by the eventsource api.
EDIT: Since posting this, I tried creating a new method in the controller that specifically handles eventsource requests, but it appears that the event response still somehow ends up in the POST response body.
public void Message(string action, string service)
{
Response.ContentType = "text/event-stream";
Response.CacheControl = "no-cache";
//Response.Write($"event: message\n");
if (action == "stop")
{
Response.Write($"data: <li> {service} has stopped </li>\n\n");
}
Response.Flush();
Thread.Sleep(1000);
Response.Close();
}
I ended up solving this. My original idea was to pass the viewmodel in each of my methods back and forth with a Dictionary<string,string> to key in each event that can be used, but the viewmodel is not persistent. I solved this issue further by implementing the events in a Dictionary saved in Session data, and the usage of Sessions for MVC can be found in the resource here that I used:
https://code.msdn.microsoft.com/How-to-create-and-access-447ada98
My final implementation looks like this:
public void Stop(ProjectViewModel model)
{
ProjectManager manager = new ProjectManager();
if (model.Servers != null && model.Servers.Count != 0)
{
string machine = model.Servers[0];
foreach (string service in model.Services)
{
manager.StopService(service, machine);
model.events.Add(service, "stopped");
this.Session["Events"] = model.events;
}
}
//return View(model);
}
public void Message(ProjectViewModel model)
{
Thread.Sleep(1000);
Response.ContentType = "text/event-stream";
Response.CacheControl = "no-cache";
Response.AddHeader("connection", "keep-alive");
var events = this.Session["Events"] as Dictionary<string, string>;
Response.Write($"event: message\n");
if (events != null && events.Count != 0)
{
foreach (KeyValuePair<string, string> message in events)
{
Response.Write($"data: {message.Key} has been {message.Value}\n\n");
}
}
Response.Flush();
Thread.Sleep(1000);
Response.Close();
}
Adding keep-alive as connection attribute in the HTTP Response header was also important to getting the SSEs to send, and the Thread.Sleep(1000)'s are used due to the stop action and message action happening simultaneously. I'm sure there's some optimizations that can go into this, but for now, this is functional and able to be further developed.

Categories

Resources