ASAT Code Tutorial 3: Build a Node.js Server, query a SatCat for weapons test debris data
Note: This is a post is part of a multi-part series.
Preface: Getting Data from Space-Track.org
In case you missed it: Please tread lightly here.
A Satellite Catalog (SatCat) contains information about objects in Earth’s orbit. The data is generated by radar tracking stations, then categorized and cataloged by some very smart analysts. We’ll get debris object data from the REST API of a SatCat operated by the U.S. Strategic Command, Space-Track.org.
If it sounds a bit serious, it is.
Air Force Maj. Gen. David D. Thompson, U.S. Strategic Command’s director of plans and policy at Offutt Air Force Base, Nebraska, said the release of new high-quality positional information on space debris of an unknown origin will help owner-operators better protect their satellites from these objects and ultimately create less space debris.
“We run a predictive program that shows where the objects are, where they will be in the future, and the potential for these objects to run into each other,” Thompson said.
Officials Expand Space-tracking Website
U.S. Department of Defense
If you adhere to the User Agreement you can obtain credentials to query the SatCat.
You’ll need a Space-Track.org username and password shortly, so if you haven’t already done so, go to space-track.org and register.
Node.js Web Service
You did notice the API usage caps on Space-Track.org, no doubt. Right?? This means if you build an app you can’t just query the servers anytime an end user requests data. In this case you’ll need to use your own web service as a buffer. It can query the Space-Track.org server and cache the result. So, let’s give that a try!.
In this post we’re going to:
- Learn how to create a skeletal web app with Node.js and the Express application framework
- Modify the root web page to display a list of satellite collision near-miss alerts for a given object
- Create a rest API that any web page (including our web page) use to get the list
- Submit requests to Space-Track.org from our server, and get a JSON response back
- Cache Space-Track.org responses per-object for 24 hours
- Store Space-Track.org username credentials in environment variables
That’s a lot, right?? Right!
Follow the bouncing ball!:
REM give the service a home
mkdir c:\git\stupidRocketTricks\node\ConjunctionService
cd /D c:\git\stupidRocketTricks\node\ConjunctionService
REM verify node.js is installed, if not get it here: https://nodejs.org/en/download/
node --version
REM create a skeletal app
npx express-generator
REM install dependencies
npm install
REM start the app
SET DEBUG=conjunctionservice:* & npm start
Now, open a web browser and go to:
http://localhost:3000/
You should see:
Express
Welcome to Express
Editing the root web page (index.html)
Rather than sending static html files, the express framework “renders” html each time a page is requested. This means it creates html on the fly, and in this case it uses a “template” for the basic page layout, and fills the template from supplied parameters.
Here the template for index.html (the main web page) is found in “views/index.jade”.
Open the ConjunctionService directory in an editor such as Visual Studio Code, then find the index.jade template and take a lil looksy:
extends layout
block content
h1= title
p Welcome to #{title}
You now see where the web page found at localhost:3000/ is coming from, … except instead of “Express” the template says “title”. So, where’s the value of “title” being set?
Find and open “routes/index.js”:
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
Adding list data
Index.js is a javascript file with a route handler that executes every time the user requests the root index.html file. The default javascript “renders” index.jade into an html file, while supplying a parameter ‘title’ the value of ‘Express". Aha!
First, change the value ‘Express’ to ‘ConjunctionService’ or whatever you choose. Now, let’s add some fake data the template can render for the time being. We will replace this data with a call to our own API, which will return cached values from the Space-Track.org server.
Find and open “routes/index.js”, then update it to:
var express = require('express');
var router = express.Router();
var conjuntionList =
[
{
CDM_ID : 1,
TCA: "2022-12-25T22:01:11.000000",
PC: 0.1111,
SAT_1_NAME : "Apollo 2022",
SAT1_OBJECT_TYPE : "PAYLOAD",
SAT_2_NAME : "Cosmos 2022",
SAT2_OBJECT_TYPE : "ROCKET BODY"
},
{
CDM_ID : 2,
TCA: "2022-12-31T22:22:22.000000",
PC: 0.2222,
SAT_1_NAME : "Apollo 2022",
SAT1_OBJECT_TYPE : "PAYLOAD",
SAT_2_NAME : "Astroman Roadster",
SAT2_OBJECT_TYPE : "DEBRIS"
}
];
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Conjunction Service', list: conjuntionList });
});
module.exports = router;
Adding the list to the web page
We’re now passing in a list of conjunctions (near-miss alerts, we just can’t see them yet.) We’ll need to edit the template to view the alerts.
Open index.jade once again, and add to the template so it reads:
extends layout
block content
h1= title
p Welcome to #{title}
ul
p
strong Active Alerts:
for alert in conjuntionList
li
strong Alert ID #{alert.CDM_ID}
p Probability:#{alert.PC} / Time of Closest Approach: #{alert.TCA}
p #{alert.SAT_1_NAME} (#{alert.SAT1_OBJECT_TYPE}) / #{alert.SAT_2_NAME} (#{alert.SAT2_OBJECT_TYPE})
If you restart node and refresh the web page, you should have it should look a bit like this:
Conjunction Service
Welcome to Conjunction Service
Active Alerts:
- Alert ID 1
Probability:0.1111 / Time of Closest Approach: 2022-12-25T22:01:11.000000
Apollo 2022 (PAYLOAD) / Cosmos 2022 (ROCKET BODY)- Alert ID 2
Probability:0.2222 / Time of Closest Approach: 2022-12-31T22:22:22.000000
Apollo 2022 (PAYLOAD) / Astroman Roadster (DEBRIS)
Schwing!! (as in: an onomatopoeia of the sound a sword makes when drawn from a metal scabbard)
Now, we need an API that supplies data to the web page’s list.
Adding an API Entry point
Find and open app.js. We’re going to add an API entry point that allows a caller to request a conjunction list for a given object id, and return the value in json.
Look for the 404 handler, it should be right after app.use('/users', usersRouter);
… We’ll insert an API entry point between the two:
app.get('/api/cj_list/:objectid', (req, res) => {
const objectid = req.params.objectid;
console.log('Received cj_list objectid=' + objectid);
var conjuntionList =
[
{
CDM_ID : 1,
TCA: "2022-12-25T22:01:11.000000",
PC: 0.1111,
SAT_1_NAME : objectid,
SAT1_OBJECT_TYPE : "PAYLOAD",
SAT_2_NAME : "Cosmos 2022",
SAT2_OBJECT_TYPE : "ROCKET BODY"
},
{
CDM_ID : 2,
TCA: "2022-12-31T22:22:22.000000",
PC: 0.2222,
SAT_1_NAME : objectid,
SAT1_OBJECT_TYPE : "PAYLOAD",
SAT_2_NAME : "Astroman Roadster",
SAT2_OBJECT_TYPE : "DEBRIS"
}
];
res.json(conjuntionList);
});
As you can follow from the code, the handler parses out an object Id, then returns some test data. Try testing the API url… Restart the app, and visit http://localhost:3000/api/cj_list/foobar
Your browser should receive some JSON, where SAT_1_NAME now reads ‘foobar’. The command line hosting the node.js app should also have printed:
Received cj_list objectid=foobar
GET /api/cj_list/foobar 200 5.346 ms - 344
Rockin’. We now have an API the web page can call.
Calling our own API from the web page
We can use axios to very easily send an http request. To use it, we’ll have to use the node package manager, npm, to install axios into our app’s directory. From the command prompt, in the application directory, invoke npm:
npm install axios
The package manager will install axios. We can now call it from our index request handler in index.js
. Near the top we’ll need to add a require(axios);
before we use it. Then, when our web page is requested we can in turn make an API call over HTTP (to our API entry point) and get the list of alerts. The full code for index.js:
var createError = require('http-errors');
var express = require('express');
var router = express.Router();
const axios = require('axios');
/* GET home page with query. */
router.get('/:objectid', function(req, res, next) {
const objectid = req.params.objectid;
if(!objectid)
{
next(createError(404));
}
else
{
// Query API:
var queryURL = req.protocol+"://"+req.headers.host + '/api/cj_list/' + objectid;
axios
.get(queryURL)
.then(response => {
console.log(`statusCode: ${response.status}`);
console.log(response);
res.render('index', { title: 'Conjunction Service', objectid: objectid, conjuntionList: response.data });
})
.catch(error => {
console.error(error);
next(createError(404));
});
}
});
module.exports = router;
Here we parse an objectid from the URL… if no object Id was supplied we’ll return a 404 error. Otherwise, we’ll use axios to make an HTTP call to our API, which will return a JSON list, parsed by express into response.data
. Then, we just supply that to jade when it renders our web page. Simple! You’ll see I’ve also added objectId as input to the Jade template, so the web page can show the object name.
If you try the web page now, it should echo whatever you pass on for object Id in the URL:
Conjunction Service
Welcome to Conjunction Service
Active Alerts for object foobar: Alert ID 1
Probability:0.1111 / Time of Closest Approach: 2022-12-25T22:01:11.000000
foobar (PAYLOAD) / Cosmos 2022 (ROCKET BODY)
Alert ID 2
Probability:0.2222 / Time of Closest Approach: 2022-12-31T22:22:22.000000
foobar (PAYLOAD) / Astroman Roadster (DEBRIS)
There’s only one thing left to do…
Calling Space-Track.org from our server (and caching the result)
Meanwhile, in app.js
, where our API is currently implemented… Let’s save/reload a results cache to the file system. A simple implementation:
var cachedResults = {};
var cachedResults = new Object();
var cacheFile = 'cache/space-track-results-cache.json';
var fileText = fs.readFileSync(cacheFile, 'utf8');
if(fileText)
{
cachedResults = JSON.parse(fileText);
}
This looks for a .json file that caches responses from the server every time the app starts. Note that you’ll need to create the file yourself, or the first time it runs you’ll get an error. And then… every time we receive an updated cache value from Space-Track.org, we’ll update the cache similarly:
fs.writeFileSync(cacheFile, JSON.stringify(cachedResults), {encoding: 'utf8'});
This implementation will cache the results (per object) and only bother the Space-Track.org server when no cached response is available.
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
const axios = require('axios');
var fs = require('fs');
var date = new Date();
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
function querySpaceTrackUrl(objectId)
{
var uriBase = 'https://www.space-track.org';
var requestController = '/basicspacedata';
var requestAction = '/query';
var predicateValues = '/class/cdm_public/TCA/%3E' + date.toISOString() + '/SAT_1_NAME/~~' + objectId + '/orderby/TCA%20asc/emptyresult/show';
return uriBase + requestController + requestAction + predicateValues;
}
function querySpaceTrack(objectId, onSuccess, onError)
{
// NEVER share your spacetrack user / password with anyone!!
// So, especially don't put them in source code.
// You can either pass them in as command line args, or better yet get them from
// environment variables.
var user = process.env.SPACETRACK_USER;
var password = process.env.SPACETRACK_PASSWORD;
var queryUrl = querySpaceTrackUrl(objectId);
var postData =
{
identity : user,
password : password,
query : queryUrl
}
// Space-Track.Org auth API URL:
var queryURL = 'https://www.space-track.org/ajaxauth/login';
// Send the REST API request to Space-Track.org with the user/pwd/query as post data
axios
.post(queryURL, postData)
.then(response => {
console.log('POST Success: ' + JSON.stringify(response.data).substring(0,255));
onSuccess(response.data);
})
.catch(error => {
console.log('POST Error: ' + error);
onError(error);
});
};
var cachedResults = new Object();
var cacheFile = 'cache/space-track-results-cache.json';
var fileText = fs.readFileSync(cacheFile, 'utf8');
if(fileText)
{
cachedResults = JSON.parse(fileText);
}
app.get('/api/cj_list/:objectid', (req, res) => {
var DataCache = new Object();
const objectid = req.params.objectid;
console.log('Received cj_list objectid=' + objectid);
// Check the cache first!
if(cachedResults[objectid] && Date.now() < cachedResults[objectid].Expires)
{
console.log('Cached Data=' + JSON.stringify(cachedResults[objectid].Data).substring(0,255));
res.json(cachedResults[objectid].Data);
return;
}
querySpaceTrack(
objectid,
// If the SpaceTrack call succeeded, cache the data and return it...
(data) =>
{
cachedResults[objectid] = new Object();
cachedResults[objectid].Expires = Date.now() + 24 * 60 * 60 * 1000;
cachedResults[objectid].Data = data;
fs.writeFileSync(cacheFile, JSON.stringify(cachedResults), {encoding: 'utf8'});
res.json(data);
},
// If the call failed, return an empty object
() => { res.json({}); }
);
});
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
(This is a very simple implementation for demonstration purposes, in no way is it meant to be more.)
Note that no usernames or passwords are stored in the app’s source code. Rather, the values are obtained from environment variables. This is a very common way for web services running in the cloud obtain credentials they need. So, before running the app you’ll need to set your Space-Track.org username and password as environment variables.
set SPACETRACK_USER=your_username
set SPACETRACK_PASSWORD=your_password
When you finish the app (or, run the example app from github) you should be able get an HTML page with a list of collision alerts caused by Cosmos-1408 and Fengyun-1C test debris.
URLS to test the server on your local machine:
http://localhost:3000/COSMOS%201408
http://localhost:3000/FENGYUN%201C
Or, if you don’t have the server working, click below to view cached results:
COSMOS-1408 (cached)
FENGYUN-1C (cached)
Active Emergency Alerts:
Russia/Cosmos/Kosmos 1408: 6 total
China/Fengyun-1C: 374 total
Note that in this implementation the server renders the entire web page before it returns any response to the client browser. This can take a while and make the page rather slow.
There’s some significant delay before any response is returned to the client, at all. However, there’s no reason that the first API call (where the node.js server calls itself) must be made on the server side. Rather, the page could immediately return a web page, with some client-side javascript to execute the initial API call after/during the page load. This would allow the page to more quickly start rendering in the browser, and worry about the data later. However, this is beyond the scope of the tutorial and it’s best handled with client-side frameworks such as Angular… But, it’s worth highlighting.
For a working example, please see the GitHub Project:
ConjunctionService Node.js Project on GitHub
Back Link:
Defense: We need clean Anti-Satellite Weapons! Part 2: Intro to Asat Code Tutorials