Compare commits

...

2 Commits

Author SHA1 Message Date
Elizabeth Cray f70142b7b0 Basic cleanup before going public
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 16:45:54 -04:00
Liz Cray a7d3af3323 Dirty GeoJSON API 2026-04-21 00:08:25 +00:00
3 changed files with 149 additions and 21 deletions
+24
View File
@@ -1,3 +1,27 @@
# DeHaze
Port the Haze Explr API to GEOJSON for use in GIS applications.
This was a quick and dirty project, and will probably get some updates slowly in the future. I put what I learned from this in the [HacDC Wiki](https://wiki.hacdc.org/en/Projects/API/HazeExplr).
## Usage
```bash
npm i
node serve.js
```
To use the endpoint, replace 'EMAIL' and 'PASSWORD' with your Haze Explr account details:
```bash
echo "EMAIL:`echo 'PASSWORD' | sha256sum | sed -e 's/ -//'`" | base64
```
Then, make a request to the endpoint with the resulting string as the value of the `auth` query parameter:
```bash
curl "http://localhost:3000/locations?auth=BASE64_ENCODED_STRING"
```
`basicauth_serve.js` does the same thing bus expects account details to be passed via basic HTTP auth, but does not work with QGIS.
+112
View File
@@ -0,0 +1,112 @@
const express = require('express');
const auth = require('express-basic-auth');
const { createHash } = require('crypto');
const app = express();
const port = 8355;
const geonJsonTemplate = {
type: "FeatureCollection",
features: [],
"marker-symbol-images": {
"star": "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 640'><path opacity='.4' fill='currentColor' d='M160 263.7L160 512C160 520.8 167.2 528 176 528L261.9 528L231.6 479.9C227.2 473 228.8 463.9 235.2 458.8L320 391.5L261.9 315.4C250.6 300.6 269.3 281.8 284.2 292.9L399.4 379.1C407.8 385.4 408 397.9 399.8 404.4L320 467.8L357.9 528L464 528C472.8 528 480 520.8 480 512L480 263.7L320 120.2L160 263.7z'/><path fill='currentColor' d='M336 70.1C326.9 61.9 313.1 61.9 304 70.1L72 278.1C62.1 286.9 61.3 302.1 70.2 312C79.1 321.9 94.2 322.7 104.1 313.8L112.1 306.6L112.1 511.9C112.1 547.2 140.8 575.9 176.1 575.9L464.1 575.9C499.4 575.9 528.1 547.2 528.1 511.9L528.1 306.6L536.1 313.8C546 322.6 561.1 321.8 570 312C578.9 302.2 578 287 568.2 278.1L528.2 242.2L528.2 152C528.2 138.7 517.5 128 504.2 128C490.9 128 480.2 138.7 480.2 152L480.2 199.2L336.2 70.1zM480 263.7L480 512C480 520.8 472.8 528 464 528L357.9 528L320 467.8L399.7 404.5C407.9 398 407.7 385.4 399.3 379.2L284.2 293C269.3 281.9 250.6 300.8 261.9 315.5L320 391.6L235.2 458.9C228.8 464 227.3 473.1 231.6 480L261.9 528.1L176 528.1C167.2 528.1 160 520.9 160 512.1L160 263.7L320 120.2L480 263.7z'/></svg>",
"star-stroked": "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'><path fill='currentColor' d='M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm73.8 149.3c7.9-22.3 29.1-37.3 52.8-37.3l58.3 0c34.9 0 63.1 28.3 63.1 63.1 0 22.6-12.1 43.5-31.7 54.8L248 280.4c-.2 13-10.9 23.6-24 23.6-13.3 0-24-10.7-24-24l0-13.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1 0-8.4-6.8-15.1-15.1-15.1l-58.3 0c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM192 368a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zM48 104a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM376 80a24 24 0 1 1 0 48 24 24 0 1 1 0-48zM48 408a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm328-24a24 24 0 1 1 0 48 24 24 0 1 1 0-48z'/></svg>"
}
};
app.use(auth({
authorizer: (username, password) => {
return true;
}
}));
app.all('/locations.geojson', (req, res) => {
// Verify Inputs
if ((!req.auth.user || !req.auth.password) && (!req.query.auth)) {
res.status(401).send('Unauthorized');
return;
}
// Query Haze API Websocket
let hazePwHash = null;
let hazeUser = null;
if (req.query.auth) {
let paramAuth = Buffer.from(req.query.auth, 'base64').toString('utf-8');
hawUser = paramAuth.split(":")[0];
hazePwHash = paramAuth.split(":")[1];
} else {
hazeUser = req.auth.user;
if (/\b[A-Fa-f0-9]{64}\b/.test(req.auth.password)) {
hazePwHash = req.auth.password;
} else {
hazePwHash = createHash('sha256').update(req.auth.password).digest('hex');
}
}
const haze = new WebSocket('wss://do.ecven.com:8120/explr');
haze.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
let haze_authToken = '', haze_username = '';
switch (data['event']) {
case "connected":
haze.send(JSON.stringify({
"event": "authenticateAccount_request",
"body": [
{
"password": hazePwHash,
"email": hazeUser
}
],
"socketMessageId": 0
}));
break;
case "authenticateAccount_response":
if (data['body']['response'] === 1) {
haze_authToken = data['body']['authToken'];
haze_username = data['body']['username'];
haze.send('{"event": "getMyLocationsRequest_request","body": [{"higlightImages": true}],"socketMessageId": 0}');
} else {
console.dir(req.query);
res.status(401).json({ ...data, password: req.auth.password });
haze.close();
res.end();
}
break;
case "getMyLocationsRequest_response":
if (data['body']['response'] === 1) {
const hazeLocations = data['body']['locations'].map(location => {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [location['coordinates']['longitude'], location['coordinates']['latitude']]
},
properties: {
id: location['id'],
title: location['name'],
"marker-symbol": location['isUnconfirmedLocation'] === 1 ? "star-stroked" : "star",
}
}
});
res.setHeader('Content-Type', 'application/json');
haze.close();
res.end(JSON.stringify({
...geonJsonTemplate,
features: hazeLocations
}));
} else {
console.dir(data);
res.status(500).send('Failed to retrieve locations');
haze.close();
res.end();
}
break;
default:
console.dir(data);
res.status(500).send('Unexpected response from Haze API');
haze.close();
res.end();
}
});
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
+13 -21
View File
@@ -1,12 +1,8 @@
const { loadEnvFile } = require('node:process');
const express = require('express');
const auth = require('express-basic-auth');
const { createHash } = require('crypto');
loadEnvFile();
const app = express();
const port = process.env.PORT || 3000;
const port = 8356;
const geonJsonTemplate = {
type: "FeatureCollection",
features: [],
@@ -16,24 +12,19 @@ const geonJsonTemplate = {
}
};
app.use(auth({
authorizer: (username, password) => {
return true;
}
}));
app.all('/locations.geojson', (req, res) => {
// Verify Inputs
if (!req.auth.user || !req.auth.password) {
if (!req.query.auth) {
res.status(401).send('Unauthorized');
return;
}
// Query Haze API Websocket
let hazePwHash = null;
if (/\b[A-Fa-f0-9]{64}\b/.test(req.auth.password)) {
hazePwHash = req.auth.password;
} else {
hazePwHash = createHash('sha256').update(req.auth.password).digest('hex');
let hazeUser = null;
if (req.query.auth) {
let paramAuth = Buffer.from(req.query.auth, 'base64').toString('utf-8').replaceAll('\n', '');
hazeUser = paramAuth.split(":")[0];
hazePwHash = paramAuth.split(":")[1];
}
const haze = new WebSocket('wss://do.ecven.com:8120/explr');
haze.addEventListener('message', (event) => {
@@ -41,25 +32,26 @@ app.all('/locations.geojson', (req, res) => {
let haze_authToken = '', haze_username = '';
switch (data['event']) {
case "connected":
haze.send(JSON.stringify({
let pld = {
"event": "authenticateAccount_request",
"body": [
{
"password": hazePwHash,
"email": req.auth.user
"email": hazeUser
}
],
"socketMessageId": 0
}));
};
haze.send(JSON.stringify(pld));
break;
case "authenticateAccount_response":
if (data['body']['response'] === 1) {
haze_authToken = data['body']['authToken'];
haze_username = data['body']['username'];
console.dir(data);
haze.send('{"event": "getMyLocationsRequest_request","body": [{"higlightImages": true}],"socketMessageId": 0}');
} else {
res.status(401).json({ ...data, password: req.auth.password });
console.dir(req.query);
res.status(401).json({ ...data, password: req.query.auth });
haze.close();
res.end();
}