Merge branch 'develop' of github.com:EDCD/EDDN into develop

This commit is contained in:
Athanasius 2022-01-28 17:31:14 +00:00
commit afddf9f849
21 changed files with 5013 additions and 43 deletions

21
.editorconfig Normal file
View File

@ -0,0 +1,21 @@
# This is the project top-level .editorconfig
root = true
# Defaults for all file types
[*]
# 4-space indents, no TABs
indent_style = space
tab_width = 4
indent_size = tab
# Hard-wrap at 120 columns
max_line_length = 119
# Unix EOL, single \n
end_of_line = lf
# UTF-8 is the only sensible option, no BOM
charset = utf-8
# All files should have a final newline
insert_final_newline = true

View File

@ -87,8 +87,9 @@ do
log " Archiving ${service}.log ..."
# We have no means to tell the service to close and re-open output, it's
# to stdout/err anyway. So we copy it.
COMPRESSED_NAME="${service}.log.$(date --iso-8601=seconds)"
cp ${service}.log "${COMPRESSED_NAME}"
TIMESTAMP="$(date --iso-8601=seconds)"
ARCHIVED_NAME="${service}.log.${TIMESTAMP}"
cp ${service}.log "${ARCHIVED_NAME}"
if [ $? -ne 0 ];
then
echo " FAILED copying live log file to new archive!!!" >&2
@ -97,8 +98,15 @@ do
fi
# Truncate the live file.
:> ${service}.log
if [ "${service}" == "gateway" ];
then
# Produce a report of interesting errors
${HOME}/.local/bin/eddn-report-log-errors "${ARCHIVED_NAME}" > "${HOME}/reports/eddn-errors/by-log-rotation/eddn-errors-${TIMESTAMP}.txt"
fi
# Now compress the newly archived log
gzip -9v "${COMPRESSED_NAME}"
gzip -9v "${ARCHIVED_NAME}"
log " DONE"
else
log " No"

13
docs/Contributing.md Normal file
View File

@ -0,0 +1,13 @@
# Contributing to the EDDN Project
## Introduction
This file is still mostly a stub.
## Text format
The project contains an `.editorconfig` file at its root. Please either ensure
your editor is taking note of those settings, or cross-check its contents
with the
[editorconfig documentation](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties)
, and ensure your editor/IDE's settings match.

View File

@ -244,6 +244,10 @@ Each `message` object must have, at bare minimum:
2. At least one other key/value pair representing the data. In general there
will be much more than this. Consult the
[schemas and their documentation](./).
3. Where the data is sourced from a Journal event please do preserve the
event key and value. Yes, where we use an event-specific schema this
might seem redundant, but it might aid an EDDN listener in streamlining
their code, and it does no harm.
Because the first versions of some schemas were defined when only the CAPI
data was available, before Journal files existed, many of the key names chosen
@ -268,6 +272,39 @@ changes to your code.
It is also advisable to Watch this repository on GitHub so as to be aware
of any changes to schemas.
#### `horizons` and `odyssey` flags
Where the schema allows for them, add the `horizons` and `odyssey`
keys with boolean values. `null` is not allowed in the values, so if
you cannot determine a value do not include that key at all. The best
source of these is the `LoadGame` event from journals. It's present
both in the PC local files and the CAPI journal data. If you're
composing a shipyard or outfitting message from CAPI data then it is
possible to synthesise the `horizons` flag. For now consult the function
`capi_is_horizons()` in
[EDMarketConnector:plugins/eddn.py](https://github.com/EDCD/EDMarketConnector/blob/stable/plugins/eddn.py)
for a method to achieve this.
As of 2022-01-22 the following was observed for the `LoadGame` events as
present in CAPI-sourced Journal files (which were confirmed to match the
PC-local files for these events):
- PC Odyssey Client, game version `4.0.0.1002`:
```json
{ "timestamp":"2022-01-20T11:15:22Z", "event":"LoadGame", "FID":"F<elided>", "Commander":"<elided>", "Horizons":true, "Odyssey":true,...
```
- PC Horizons Client, game version `3.8.0.403`, no `Odyssey` key was
present:
```json
{ "timestamp":"2022-01-20T11:20:17Z", "event":"LoadGame", "FID":"F<elided>", "Commander":"<elided>", "Horizons":true,...
```
- PC 'base' Client, game version `3.8.0.403`, no `Odyssey` key was
present:
```json
{ "timestamp":"2022-01-20T11:22:54Z", "event":"LoadGame", "FID":"F<elided>", "Commander":"<elided>", "Horizons":false,...
```
### Server responses
There are three possible sources of HTTP responses when sending an upload
to EDDN.
@ -299,8 +336,10 @@ make a valid request" responses you might experience the following:
#### bottle responses
1. `413` - `Payload Too Large` - `bottle` enforces a maximum request size
and the request exceeds that. As of 2022-01-07 the limit is 1MiB, and
pertains to the plain-text size, not after gzip compression if used.
and the request exceeds that. As of 2022-01-07 the limit is 1MiB,
which is versus the compressed size of the body, if compression is
used. Thus compression *will* allow for sending approximately 10x
larger messages.
To verify the current limit check for the line that looks like:
```

View File

@ -0,0 +1,33 @@
# EDDN ApproachSettlement Schema
## Introduction
Here we document how to take data from an ED `ApproachSettlement` Journal
Event and properly structure it for sending to EDDN.
Please consult [EDDN Schemas README](./README-EDDN-schemas.md) for general
documentation for a schema such as this.
## Senders
The primary data source for this schema is the ED Journal event
`ApproachSettlement`.
### Key Renames
Many of the key names have a different case defined in this schema, make
sure you are renaming them as appropriate.
### Elisions
None
### Augmentations
#### horizons flag
You SHOULD add this key/value pair, using the value from the `LoadGame` event.
#### odyssey flag
You SHOULD add this key/value pair, using the value from the `LoadGame` event.
#### StarSystem
You MUST add a StarSystem key/value pair representing the name of the system this event occurred in. Source this from either Location, FSDJump or CarrierJump as appropriate.
#### StarPos
You MUST add a `StarPos` array containing the system co-ordinates from the last `FSDJump`, `CarrierJump`, or `Location` event.

View File

@ -0,0 +1,93 @@
{
"$schema" : "http://json-schema.org/draft-04/schema#",
"id" : "https://eddn.edcd.io/schemas/approachsettlement/1#",
"type" : "object",
"additionalProperties" : false,
"required": [ "$schemaRef", "header", "message" ],
"properties": {
"$schemaRef": {
"type" : "string"
},
"header": {
"type" : "object",
"additionalProperties" : true,
"required" : [ "uploaderID", "softwareName", "softwareVersion" ],
"properties" : {
"uploaderID": {
"type" : "string"
},
"softwareName": {
"type" : "string"
},
"softwareVersion": {
"type" : "string"
},
"gatewayTimestamp": {
"type" : "string",
"format" : "date-time",
"description" : "Timestamp upon receipt at the gateway. If present, this property will be overwritten by the gateway; submitters are not intended to populate this property."
}
}
},
"message": {
"type" : "object",
"description" : "Contains all properties from the listed events in the client's journal minus the Localised strings and the properties marked below as 'disallowed'",
"additionalProperties" : false,
"required" : [ "timestamp", "event", "StarSystem", "StarPos", "SystemAddress", "Name", "MarketID", "BodyID", "BodyName", "Latitude", "Longitude" ],
"properties" : {
"timestamp": {
"type" : "string",
"format" : "date-time"
},
"event" : {
"enum" : [ "ApproachSettlement" ]
},
"horizons": {
"type" : "boolean",
"description" : "Whether the sending Cmdr has a Horizons pass."
},
"odyssey": {
"type" : "boolean",
"description" : "Whether the sending Cmdr has an Odyssey expansion."
},
"StarSystem": {
"type" : "string",
"minLength" : 1,
"description" : "Must be added by the sender"
},
"StarPos": {
"type" : "array",
"items" : { "type": "number" },
"minItems" : 3,
"maxItems" : 3,
"description" : "Must be added by the sender"
},
"SystemAddress": {
"type" : "integer"
},
"Name" : {
"type" : "string",
"description" : "Name of settlement"
},
"MarketID": {
"type" : "integer"
},
"BodyID": {
"type" : "integer"
},
"BodyName": {
"type" : "string"
},
"Latitude": {
"type" : "number"
},
"Longitude": {
"type" : "number"
}
}
}
},
"definitions": {
"disallowed" : { "not" : { "type": [ "array", "boolean", "integer", "number", "null", "object", "string" ] } }
}
}

View File

@ -0,0 +1,30 @@
# EDDN FSSAllBodiesFound Schema
## Introduction
Here we document how to take data from an ED `FSSAllBodiesFound` Journal
Event and properly structure it for sending to EDDN.
Please consult [EDDN Schemas README](./README-EDDN-schemas.md) for general
documentation for a schema such as this.
## Senders
The primary data source for this schema is the ED Journal event
`FSSAllBodiesFound`.
### Key Renames
Many of the key names have a different case defined in this schema, make
sure you are renaming them as appropriate.
### Elisions
None
### Augmentations
#### horizons flag
You SHOULD add this key/value pair, using the value from the `LoadGame` event.
#### odyssey flag
You SHOULD add this key/value pair, using the value from the `LoadGame` event.
#### StarPos
You MUST add a `StarPos` array containing the system co-ordinates from the
last `FSDJump`, `CarrierJump`, or `Location` event.

View File

@ -0,0 +1,78 @@
{
"$schema" : "http://json-schema.org/draft-04/schema#",
"id" : "https://eddn.edcd.io/schemas/fssallbodiesfound/1#",
"type" : "object",
"additionalProperties" : false,
"required": [ "$schemaRef", "header", "message" ],
"properties": {
"$schemaRef": {
"type" : "string"
},
"header": {
"type" : "object",
"additionalProperties" : true,
"required" : [ "uploaderID", "softwareName", "softwareVersion" ],
"properties" : {
"uploaderID": {
"type" : "string"
},
"softwareName": {
"type" : "string"
},
"softwareVersion": {
"type" : "string"
},
"gatewayTimestamp": {
"type" : "string",
"format" : "date-time",
"description" : "Timestamp upon receipt at the gateway. If present, this property will be overwritten by the gateway; submitters are not intended to populate this property."
}
}
},
"message": {
"type" : "object",
"description" : "Contains all properties from the listed events in the client's journal, minus the Localised strings and the properties marked below as 'disallowed'",
"additionalProperties" : false,
"required" : [ "timestamp", "event", "SystemName", "StarPos", "SystemAddress", "Count" ],
"properties" : {
"timestamp": {
"type" : "string",
"format" : "date-time"
},
"event" : {
"enum" : [ "FSSAllBodiesFound" ]
},
"horizons": {
"type" : "boolean",
"description" : "Whether the sending Cmdr has a Horizons pass."
},
"odyssey": {
"type" : "boolean",
"description" : "Whether the sending Cmdr has an Odyssey expansion."
},
"SystemName": {
"type" : "string",
"minLength" : 1
},
"StarPos": {
"type" : "array",
"items" : { "type": "number" },
"minItems" : 3,
"maxItems" : 3,
"description" : "Must be added by the sender if not present in the journal event"
},
"SystemAddress": {
"type" : "integer",
"description" : "Should be added by the sender if not present in the journal event"
},
"Count" : {
"type" : "integer",
"description" : "Number of bodies in this system"
}
}
}
},
"definitions": {
"disallowed" : { "not" : { "type": [ "array", "boolean", "integer", "number", "null", "object", "string" ] } }
}
}

View File

@ -105,6 +105,23 @@ def process_file(input_file: str) -> None:
print(matches.group('err_msg'))
print(line)
elif matches.group('schema_ref') == 'https://eddn.edcd.io/schemas/journal/1':
# <https://github.com/EDCD/EDMarketConnector/issues/1403>
if matches.group('err_msg') == 'Failed Validation "[<ValidationError: "\'SystemAddress\' is a required property">]"':
# <https://github.com/EDCD/EDMarketConnector/issues/1403>
pass
elif matches.group('err_msg').startswith(
'Failed Validation "[<ValidationError: "{\'type\': [\'array\', \'boolean\', \'integer\', \'number\', \'null\', \'object\', \'string\']} is not allowed for'
):
# <https://github.com/EDCD/EDMarketConnector/issues/1403>
pass
elif matches.group('schema_ref') == 'https://eddn.edcd.io/schemas/fssdiscoveryscan/1':
if matches.group('err_msg') == 'Failed Validation "[<ValidationError: "None is not of type \'boolean\'">]"':
# <https://github.com/EDCD/EDMarketConnector/issues/1403>
pass
else:
print(line)
@ -179,6 +196,12 @@ def process_file(input_file: str) -> None:
if matches.group('software_version') == '2.0.0.660':
print(line)
# Abandoned/unmaintained project
# <https://forums.frontier.co.uk/threads/release-eva-elite-virtual-assistant-for-iphone-ipad-no-longer-working-jan-2020.245900/page-18>
# <https://apps.apple.com/gb/app/eva/id1098763533>
elif matches.group('software_name') in ('EVA [iPhone]', 'EVA [iPad]'):
pass
###################################################################
# Issues we know about, but haven't yet alerted developers to
###################################################################

View File

@ -0,0 +1,23 @@
{
"$schemaRef": "https://eddn.edcd.io/schemas/approachsettlement/1",
"header": {
"uploaderID": "from Athanasius Testing",
"softwareName": "Athanasius Testing script",
"softwareVersion": "v0.0.1"
},
"message": {
"timestamp":"2021-10-14T12:37:54Z",
"event":"ApproachSettlement",
"Name":"Arnold Defence Base",
"MarketID":3915738368,
"SystemAddress":2381282543963,
"StarSystem": "Ix",
"BodyID":32,
"BodyName":"Ix 5 a a",
"Latitude":17.090912,
"Longitude":160.236679,
"StarPos": [
-65.21875 , 7.75 , -111.03125
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
{"$schemaRef":"https://eddn.edcd.io/schemas/commodity/3","header":{"softwareName":"E:D Market Connector Windows","softwareVersion":"5.3.0-beta4extra","uploaderID":"abcdefghijklm"},"message":{"systemName":"delphi","stationName":"The Oracle","marketId":128782803,"timestamp":"2022-01-26T12:00:00Z","commodities":[]}}

View File

@ -0,0 +1,18 @@
{
"$schemaRef": "https://eddn.edcd.io/schemas/fssallbodiesfound/1",
"message": {
"timestamp":"2022-01-26T16:21:00Z",
"event":"FSSAllBodiesFound",
"SystemName":"Zeta Doradus",
"StarPos": [
30.40625,-22.65625,-2.18750
],
"SystemAddress":44853889387,
"Count":1
},
"header": {
"uploaderID": "from Athanasius Testing",
"softwareName": "Athanasius Testing script",
"softwareVersion": "v0.0.1"
}
}

View File

@ -0,0 +1 @@
{"$schemaRef":"https://eddn.edcd.io/schemas/shipyard/2","header":{"softwareName":"E:D Market Connector Windows","softwareVersion":"5.3.0-beta4extra","uploaderID":"abcdefghijklm"},"message":{"systemName":"delphi","stationName":"The Oracle","marketId":128782803,"timestamp":"2022-01-26T12:00:00Z","ships":[]}}

View File

@ -163,16 +163,6 @@ shutil.copy(
'%s/start-eddn-%s-service' % ( START_SCRIPT_BIN, setup_env.EDDN_ENV )
)
# Ensure the service log file archiving script is in place
print """
******************************************************************************
Ensuring the service log file archiving script is in place
"""
shutil.copy(
'contrib/eddn-logs-archive',
START_SCRIPT_BIN
)
# Ensure the latest monitor files are in place
old_umask = os.umask(022)
print """

View File

@ -26,6 +26,7 @@ Architecture:
4. `push_message()` then sends the message to the configured live/real
Gateway.
"""
import argparse
import gevent
import hashlib
import logging
@ -47,8 +48,15 @@ from bottle import Bottle, run, request, response, get, post
app = Bottle()
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)
__logger_channel = logging.StreamHandler()
__logger_formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(module)s:%(lineno)d: %(message)s'
)
__logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
__logger_formatter.default_msec_format = '%s.%03d'
__logger_channel.setFormatter(__logger_formatter)
logger.addHandler(__logger_channel)
logger.info('Made logger')
@ -57,6 +65,27 @@ from eddn.core.StatsCollector import StatsCollector
statsCollector = StatsCollector()
statsCollector.start()
def parse_cl_args():
parser = argparse.ArgumentParser(
prog='Gateway',
description='EDDN Gateway server',
)
parser.add_argument(
'--loglevel',
help='Logging level to output at',
)
parser.add_argument(
'-c', '--config',
metavar='config filename',
nargs='?',
default=None,
)
return parser.parse_args()
def push_message(message_body):
"""
Spawned as a greenlet to push messages (strings) through ZeroMQ.
@ -243,8 +272,12 @@ class CustomLogging(object):
return _log_to_logger
def main():
cl_args = parse_cl_args()
if cl_args.loglevel:
logger.setLevel(cl_args.loglevel)
logger.info('Loading config...')
loadConfig()
loadConfig(cl_args)
logger.info('Installing EnableCors ...')
app.install(EnableCors())

View File

@ -4,6 +4,7 @@
Contains the necessary ZeroMQ socket and a helper function to publish
market data to the Announcer daemons.
"""
import argparse
import gevent
import hashlib
import logging
@ -51,6 +52,27 @@ statsCollector = StatsCollector()
statsCollector.start()
def parse_cl_args():
parser = argparse.ArgumentParser(
prog='Gateway',
description='EDDN Gateway server',
)
parser.add_argument(
'--loglevel',
help='Logging level to output at',
)
parser.add_argument(
'-c', '--config',
metavar='config filename',
nargs='?',
default=None,
)
return parser.parse_args()
def extract_message_details(parsed_message):
uploader_id = '<<UNKNOWN>>'
software_name = '<<UNKNOWN>>'
@ -138,6 +160,7 @@ def get_decompressed_message():
# Auto header checking.
logger.debug('Trying zlib.decompress (15 + 32)...')
message_body = zlib.decompress(request.body.read(), 15 + 32)
except zlib.error:
logger.error('zlib.error, trying zlib.decompress (-15)')
# Negative wbits suppresses adler32 checksumming.
@ -149,12 +172,14 @@ def get_decompressed_message():
# body. If it's not form-encoded, this will return an empty dict.
form_enc_parsed = urlparse.parse_qs(message_body)
if form_enc_parsed:
logger.debug('Request is form-encoded')
logger.info('Request is form-encoded, compressed, from %s' % (get_remote_address()))
# This is a form-encoded POST. The value of the data attrib will
# be the body we're looking for.
try:
message_body = form_enc_parsed['data'][0]
except (KeyError, IndexError):
logger.error('form-encoded, compressed, upload did not contain a "data" key. From %s', get_remote_address())
raise MalformedUploadError(
"No 'data' POST key/value found. Check your POST key "
"name for spelling, and make sure you're passing a value."
@ -170,7 +195,7 @@ def get_decompressed_message():
# POST key/vals, or un-encoded body.
data_key = request.forms.get('data')
if data_key:
logger.debug('form-encoded POST request detected...')
logger.info('Request is form-encoded, uncompressed, from %s' % (get_remote_address()))
# This is a form-encoded POST. Support the silly people.
message_body = data_key
@ -342,7 +367,12 @@ class EnableCors(object):
def main():
loadConfig()
cl_args = parse_cl_args()
if cl_args.loglevel:
logger.setLevel(cl_args.loglevel)
loadConfig(cl_args)
configure()
app.install(EnableCors())

View File

@ -4,6 +4,7 @@
Monitor sit below gateways, or another relay, and simply parse what it receives over SUB.
"""
from threading import Thread
import argparse
import zlib
import gevent
import simplejson
@ -27,6 +28,26 @@ if Settings.RELAY_DUPLICATE_MAX_MINUTES:
duplicateMessages.start()
def parse_cl_args():
parser = argparse.ArgumentParser(
prog='Gateway',
description='EDDN Gateway server',
)
parser.add_argument(
'--loglevel',
help='CURRENTLY NO EFFECT - Logging level to output at',
)
parser.add_argument(
'-c', '--config',
metavar='config filename',
nargs='?',
default=None,
)
return parser.parse_args()
def date(__format):
d = datetime.datetime.utcnow()
return d.strftime(__format)
@ -237,7 +258,9 @@ class EnableCors(object):
return _enable_cors
def main():
loadConfig()
cl_args = parse_cl_args()
loadConfig(cl_args)
m = Monitor()
m.start()
app.install(EnableCors())

View File

@ -4,20 +4,32 @@
Relays sit below an announcer, or another relay, and simply repeat what
they receive over PUB/SUB.
"""
# Logging has to be configured first before we do anything.
import argparse
import gevent
import hashlib
import logging
import simplejson
import time
import uuid
import zlib
from threading import Thread
import time
logger = logging.getLogger(__name__)
import zlib
import gevent
import simplejson
import hashlib
import uuid
import zmq.green as zmq
# Logging has to be configured first before we do anything.
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
__logger_channel = logging.StreamHandler()
__logger_formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(module)s:%(lineno)d: %(message)s'
)
__logger_formatter.default_time_format = '%Y-%m-%d %H:%M:%S'
__logger_formatter.default_msec_format = '%s.%03d'
__logger_channel.setFormatter(__logger_formatter)
logger.addHandler(__logger_channel)
logger.info('Made logger')
from eddn.conf.Settings import Settings, loadConfig
from gevent import monkey
@ -25,6 +37,7 @@ monkey.patch_all()
from bottle import Bottle, get, request, response, run
app = Bottle()
# This import must be done post-monkey-patching!
from eddn.core.StatsCollector import StatsCollector
statsCollector = StatsCollector()
@ -37,6 +50,26 @@ if Settings.RELAY_DUPLICATE_MAX_MINUTES:
duplicateMessages.start()
def parse_cl_args():
parser = argparse.ArgumentParser(
prog='Gateway',
description='EDDN Gateway server',
)
parser.add_argument(
'--loglevel',
help='Logging level to output at',
)
parser.add_argument(
'-c', '--config',
metavar='config filename',
nargs='?',
default=None,
)
return parser.parse_args()
@app.route('/stats/', method=['OPTIONS', 'GET'])
def stats():
stats = statsCollector.getSummary()
@ -171,7 +204,12 @@ class EnableCors(object):
def main():
loadConfig()
cl_args = parse_cl_args()
if cl_args.loglevel:
logger.setLevel(cl_args.loglevel)
loadConfig(cl_args)
r = Relay()
r.start()

View File

@ -1,6 +1,5 @@
# coding: utf8
import argparse
import simplejson
from eddn.conf.Version import __version__ as version
@ -74,6 +73,11 @@ class _Settings(object):
"https://eddn.edcd.io/schemas/navroute/1" : "schemas/navroute-v1.0.json",
"https://eddn.edcd.io/schemas/navroute/1/test" : "schemas/navroute-v1.0.json",
"https://eddn.edcd.io/schemas/approachsettlement/1" : "schemas/approachsettlement-v1.0.json",
"https://eddn.edcd.io/schemas/approachsettlement/1/test" : "schemas/approachsettlement-v1.0.json",
"https://eddn.edcd.io/schemas/fssallbodiesfound/1" : "schemas/fssallbodiesfound-v1.0.json",
"https://eddn.edcd.io/schemas/fssallbodiesfound/1/test" : "schemas/fssallbodiesfound-v1.0.json",
}
GATEWAY_OUTDATED_SCHEMAS = [
@ -134,15 +138,14 @@ class _Settings(object):
Settings = _Settings()
def loadConfig():
'''
Loads in a settings file specified on the commandline if one has been specified.
def loadConfig(cl_args):
"""
Load in a commandline-specified settings file, if applicable.
A convenience method if you don't need other things specified as commandline
options. Otherwise, point the filename to Settings.loadFrom().
'''
parser = argparse.ArgumentParser()
parser.add_argument("-c", "--config", nargs="?", default=None)
args = parser.parse_args()
if args.config:
Settings.loadFrom(args.config)
:param cl_args: An `argparse.parse_args()` return.
"""
if cl_args.config:
Settings.loadFrom(cl_args.config)