Reflections on small-scale web services
Posted on 2021-08-08 | Last updated 2021-08-08
|
Tags:
ProgrammingWeb-Development
I recently made a Lines of Code badge publishing solution, whose purpose was to take a git project, count its LoC, and show the image out to the user in the form of a github badge.
This led me into the thought process of which web framework would be the most capable to do the following
What was considered
This led me into the thought process of which web framework would be the most capable to do the following
- Display a picture out to the user
- Execute shell programs safely from API calls
- Simple to use
What was not considered
Given the requirements, I needed something simple, that could be put up in as few lines of code as possible. Therefore bigger frameworks that I have used previously like .Net Core did not enter my mind. A possible candidate i overlooked was node.js, because I have no experience with it; Also, recently, golang has become popular in my circles for this purpose, but alas I did not consider it either.What was considered
Apache/PHP
The first thought that entered my mind when I thought of what to do was PHP, as it comes with Apache, and is capable of all the requirements. My web-server runs Apache, and therefore it would be very simple to just send inobstudios.com/LOCCounterBadge/ directly to a PHP script, and I would not even need a separate web-server for the app. Perfect! I thought.
Then, I remembered that I had done something similar for the CI process for this very website, and shell execution in PHP is neither pretty nor secure.
shell_exec('(cd '.escapeshellarg($source_dir).'/server-files && sudo -u '.escapeshellarg($user).' ./update-server-version.py '.escapeshellarg($service_name).' '.escapeshellarg($tag).')');
is the way the PHP wiki says to deal with this. Not exactly how I envision safe passing of arguments from user-submitted data. Luckily, in said code the user had to be authenticated before it would consider the arguments, which made the security risk acceptable.
For a proper web-app, I would like a more relaxed approach; One where the language itself makes sure that the arguments do what they're supposed to do, and even if something gets injected it gets dealt with. The end of the previous example gives a hint of what the next thing I considered was, which also ended up being the solution.
Python
def fetch_new_data(name, branch):
proc = subprocess.run(['git', '-C', os.path.join(repos_dir, name), 'fetch'])
if proc.returncode != 0:
return False
proc = subprocess.run(['git', '-C', os.path.join(repos_dir, name), 'checkout', 'origin/'+branch])
return proc.returncode == 0
Python on the other hand has a great implementation for shell arguments with the subprocess module. It is not up to the developer to make sure that all arguments are escaped; Simply by design, you cannot create security risks.
Now that I knew that python had a good way of dealing with shell execution, I just needed to see if there were any capable web server frameworks available. And just so it happens, there are two big web-frameworks for python. Django, and Flask.
I had heard of django before, so that's where I went first, but it quickly proved too heavy-weight for my use-case. I just needed a small web-framework that would allow routing, and could parse models.
Flask on the other hand was just what I needed. In just a few lines of code I could register my endpoints and start a server
config = parse_config('conf.d', 'config.cfg')
app = flask.Flask(__name__)
app.register_blueprint(construct_responses(config, memory))
app.run(port=config.get('general', 'port', fallback=20300),
host=config.get('general', 'host', fallback='0.0.0.0'))
def construct_responses(config, memory):
response_functions = Blueprint('response_functions', __name__)
@response_functions.route('/<repository>/responses/shields_v1', methods=['GET'])
def shields_v1(repository):
if response := helpers.verify_config_response(repository, config, 'response_shields_v1'):
return response
data = json.loads(config.get(repository, 'response_shields_v1'))
if not memory.get(repository):
make_response('Could not get repository LOC', 500)
data['message'] = '{:,}'.format(memory[repository])
return make_response(data, 200)
return response_functions
So that ended up being the conclusion. If I in the future want to make a simple web-app, Flask is the go-to framework for me. Since it is written in python, there are countless libraries to help you along the way. Although beware, this is only the case for a very specific project, where traffic is a non-issue, and the functionality is simple. In any other case I would recommend a proper web-framework like C#'s Asp.Net Core or PHP's Symfony.
shell_exec('(cd '.escapeshellarg($source_dir).'/server-files && sudo -u '.escapeshellarg($user).' ./update-server-version.py '.escapeshellarg($service_name).' '.escapeshellarg($tag).')');
def fetch_new_data(name, branch):
proc = subprocess.run(['git', '-C', os.path.join(repos_dir, name), 'fetch'])
if proc.returncode != 0:
return False
proc = subprocess.run(['git', '-C', os.path.join(repos_dir, name), 'checkout', 'origin/'+branch])
return proc.returncode == 0
config = parse_config('conf.d', 'config.cfg')
app = flask.Flask(__name__)
app.register_blueprint(construct_responses(config, memory))
app.run(port=config.get('general', 'port', fallback=20300),
host=config.get('general', 'host', fallback='0.0.0.0'))
def construct_responses(config, memory):
response_functions = Blueprint('response_functions', __name__)
@response_functions.route('/<repository>/responses/shields_v1', methods=['GET'])
def shields_v1(repository):
if response := helpers.verify_config_response(repository, config, 'response_shields_v1'):
return response
data = json.loads(config.get(repository, 'response_shields_v1'))
if not memory.get(repository):
make_response('Could not get repository LOC', 500)
data['message'] = '{:,}'.format(memory[repository])
return make_response(data, 200)
return response_functions