├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── config ├── api ├── api.ini ├── api.service └── cert │ └── .gitignore ├── requirements.txt └── src ├── client ├── .gitignore ├── client.c └── test.sh └── server ├── __init__.py ├── testserver.py └── wsgi.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # virtualenv 2 | web_test_env/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorial: Write your own C client and Python server 2 | 3 | A Tutorial/Example of how you can write a web client in C that connects to any web server using an encrypted https connection. 4 | Furthermore, we're going to write a Flask web server and deploy it using Nginx. We'll also generate self-signed certificates for test purposes. 5 | 6 | **If you like this kind of tutorials make sure you give it a star so I see your interest. If you've any problems please create an issue so we can help you.** 7 | 8 | ![test](https://user-images.githubusercontent.com/25117793/90982022-bdaddd80-e564-11ea-8b88-c5b13c692214.gif) 9 | 10 | **The first part of the tutorial (setting up the server) can be quite complicated if you don't know what you're doing. If you are just interested in making the client you can skip the first part and use a service like [Random API](https://www.random.org/) to make requests with the client.** 11 | 12 | ## Contents 13 | * [Important Notes](#Important-Notes) 14 | * [File Structure](#File-Structure) 15 | * [Building a web server for testing the client](#Build-the-web-server) 16 | (if you don't need a web server for testing you can jump to [the next step](#Write-the-web-client)) 17 | * [Write a tiny web service with the Python Flask library](#Write-a-tiny-web-service-using-Flask) 18 | * [Deploy the service using uWSGI and Nginx](#Deploy-the-service-using-uWSGI-and-Nginx) 19 | * [Create your own CA (Certificate Authority) and generate self-signed certificates for the https connection](#Create-own-CA-with-self-signed-certificates) 20 | * [Write the web client](#Write-the-web-client) 21 | * [Curl and Libcurl](#Curl-and-Libcurl) 22 | * [Implement the client](#Implement-the-client) 23 | 24 | ## Important Notes 25 | 26 | * I'm using `Linux (Ubuntu 18.04)` to run the server and the client. 27 | * **I'll show you how to create self-signed certificates. If you build a web service for production don't use this method.** Instead use a CA like Let's Encrypt (it's also free). 28 | * This tutorial is based on the current repository. The file structure of the repo is explained below.
29 | * **There are 2 possible ways you can test out the code/build something upon it:** 30 | 1. Downloading this repo (using it as a template) and following the instructions/explanations below to run it (there are some files you need to create for the server such as certificates, otherwise it will not work). I'll explain how to configure everything by using this repo as some kind of template. If you only want the client then you just need to compile the `client.c` file. 31 | 2. Writing everything by yourself without downloading the repo (more work to do). 32 | * Before you start setting up the server (web service) please think where you want it to run. Later you will need the hostname of the computer where you want to run the server (for configurations and sending requests). If you want it to run on your local machine (same as client) your hostname will be `localhost` or `127.0.0.1` (ip address). If you want the server to run on a different machine than the client (but still in your home network) you need to use either that machines local ip address or the local hostname. I'll run the server on my homeserver whose hostname is `api` or `api.fritz.box` (fritz.box is the local domain of my router which will forward the requests to the api homeserver). 33 | 34 | ## File Structure 35 | 36 | This is the file structure of the project which may help you to understand the config files: 37 | 38 | c-client-flask-server-test/ (root)
39 | | - requirements.txt (all python3 pip requirements you need to install)
40 | | - config/ (config files)
41 |      42 | | - api (nginx backup file)
43 |      44 | | - api.ini (uwsgi config file)
45 |      46 | | - api.service (uwsgi service backup file)
47 |      48 | | - cert/ (the tutorial shows how to generate all these files)
49 |          50 | | - api.crt
51 |          52 | | - api.csr
53 |          54 | | - api.key
55 |          56 | | - myApiCA.key
57 |          58 | | - myApiCA.pem
59 |          60 | | - myApiCA.srl
61 | | - src/ (source code)
62 |      63 | | - client/
64 |          65 | | - client.c
66 |      67 | | - server/
68 |          69 | | - \_\_init\_\_.py
70 |          71 | | - testserver.py
72 |          73 | | - wsgi.py
74 | 75 | ## Build the web server 76 | Since I decided to run the web service on a dedicated server I need to connect to the server via ssh. In my case: 77 | ``` ssh cedric@api.fritz.box ``` 78 | If you also use a dedicated server you need to download the repo on both machines. 79 | Now we can jump into the project folder: 80 | ``` 81 | cd c-client-flask-server-test 82 | ``` 83 | ### Write a tiny web service using Flask 84 | Let's start by writing the web service and test it with a normal web browser. Later we'll connect to it via the C client. My idea of the service is that we can request a certain route and it responds with JSON giving us the request method (e.g. GET) and the specified request route. For example, if the server address is 'localhost' a request like http://localhost/hello-world (route is '/hello-world') should give the following response: `{method: "GET", path: "hello-world"}` 85 | We're going to write this little echo-service in Python using a great library that becomes more and more popular - Flask. 86 | First, we need to set up a virtual python environment. I'll do this using virtualenv. 87 | ``` 88 | python3 -m pip install virtualenv 89 | ``` 90 | If you are not familiar with pip and virtualenv check out this link: https://www.dabapps.com/blog/introduction-to-pip-and-virtualenv-python/ 91 | I called my virtual environment 'web_test_env'. Create an environment inside the project folder: 92 | ``` 93 | virtualenv web_test_env 94 | ``` 95 | Activate the environment: 96 | ``` 97 | source web_test_env/bin/activate 98 | ``` 99 | Then we can install all requirements that we're going to need (The requirements file is called 'requirements.txt'): 100 | ``` 101 | pip install -r requirements.txt 102 | ``` 103 | #### Flask Server Source Code 104 | The server's source code is located here: **src/sever/testserver.py** 105 | Let's have a look at the imports: 106 | ```python 107 | from flask import Flask 108 | from flask import request 109 | from flask import jsonify 110 | from markupsafe import escape 111 | ``` 112 | `Flask` is the class that will help us creating the web server. It is a micro web framework written in Python. With `request` we can receive information about the current request which is made by someone connecting to our service. `Jsonify` helps us converting text or datastructures to JSON. JSON is a standard for exchanging information through the web. 113 | `Escape` is just a tool that formats any text into html readable text. We're just using this to prevent bugs. 114 | Next we take a look at the actual code (it's pretty simple): 115 | ```python 116 | app = Flask(__name__) 117 | 118 | @app.route('/',methods=['GET','POST']) # defining routes 119 | def hello_world(yourtext): 120 | return jsonify(req_text=escape(yourtext), req_method=request.method) 121 | 122 | if __name__ == '__main__': 123 | app.run(host='0.0.0.0',debug=False) 124 | ``` 125 | We create our Flask instance and call it app. This object represents the service. Next, we define our routes so what happens if we call something like ```http:///some-path```. Since we want to allow every route we use a placeholder and call it ``\*. A placeholder is like a variable that contains a requested route. Next, we define a function that gets called when a route is accessed. Every time someone accesses our service the `def hello_world(yourtext)` method gets called where `yourtext` is the route that got called (same name as the placeholder). `hello_world` just returns a JSON text with the request method of the caller and the route that was requested. Since hello_world is a route function (we annotated it with @app.route(...)) every return will not just end the function but will send the returned text as a response back to the caller (client, web browser, etc.).
126 | The last two lines are used for running the service directly. Flask is a decent web server on its own so we can test our program by running 127 | ```python src/server/testserver.py``` 128 | 129 | ![](https://user-images.githubusercontent.com/25117793/90982125-69efc400-e565-11ea-9273-3e098995bda9.png "") 130 | Note that it will warn you that Flask isn't a production server but for the first test, it's sufficient. You can access the web service now by typing `http://:5000/` in your browser. In my case `http://api.fritz.box:5000/this-is-a-route`. The default port to access a Flask server is 5000 so it's important to specify the port in the URL with `:5000`. Later we will use the default http \[https] port 80 \[443]. If you cannot reach the server you need to make sure that your firewall allows port 5000. You can see all allowed ports with `sudo ufw status`. To allow port 5000 enter: 131 | ``` 132 | sudo ufw allow 5000 133 | ``` 134 | 135 | ![](https://user-images.githubusercontent.com/25117793/90982123-68260080-e565-11ea-964c-a647939d0214.png "") 136 | \*Note: We have not defined what happens if we call the root route which is just `/`. In this case the server will respond with an error. If you want to define a root route you need to do add a second route function like: 137 | ```python 138 | @app.route('/') 139 | def function_for_root_route(): 140 | return "blablabla" 141 | 142 | ``` 143 | So we are done with the server, right? Not really. There are some problems we need to look at. If you just need this server for testing purposes you can skip the following steps, but at the moment our server is highly vulnerable and unstable: 144 | 1. The built-in Flask server is slow and scales up poorly since it's not capable of handling multiple connections at once. If we aim to use it in a real-world application we cannot use the Flask server. 145 | 2. The connection is not secure. We are using the HTTP protocol which does not encrypt our requests and responses. Today most services even if they don't deal with private data use an encrypted TLS connection via the HTTPS protocol (as long as we're using our service in a local network that's not a huge problem but it still can be a security risk). 146 | 147 | In the next 2 steps, we will address these problems. 148 | ### Deploy the service using uWSGI and Nginx 149 | This tutorial is not about all the details involving uWSGI and NGINX, but I'll give a short overview of these 2 things. 150 | Nginx is a web server that is next to Apache2 the biggest open source web server in the world. It offers increased security, better performance, encrypted connections, and much more. So Nginx is a great choice, right? 151 | 152 | Well, there is a downside. In our case, the problem is Nginx cannot directly talk to a Flask application. So we will use Nginx not as a pure web server but as a [reverse proxy](https://en.wikipedia.org/wiki/Reverse_proxy). It will forward all connections to an application server called uWSGI that can talk to our Flask app over the WSGI protocol. This approach is widely used and gives extra security because of multiple services having to talk to each other. 153 | uWSGI can also be used as a standalone solution but Nginx gives us more possibilities in the future and is very popular so it won't do any harm learning about it. 154 | 155 | #### uWSGI 156 | The first step is to set up and configure the uWSGI server. The entry point for uWSGI is defined in **src/server/wsgi.py**: 157 | ```python 158 | from testserver import app 159 | 160 | if __name__ == '__main__': 161 | app.run() 162 | ``` 163 | It's like a second main function that uWSGI uses to run the code in `testserver.py`. 164 | 165 | Next, we test our server now by running the following command in our root directory: 166 | ``` 167 | uwsgi --socket 0.0.0.0:5000 --protocol=http --chdir src/server/ -w wsgi:app 168 | ``` 169 | Our service should be available under `http://:5000/` but now running on uWSGI instead of the built-in Flask server. 170 | 171 | Now we can create a config file that contains all parameters instead of using the command above. My config is in config/api.ini: 172 | ``` 173 | [uwsgi] 174 | chdir = /home/cedric/c-client-flask-server-test/src/server/ # file to the wsgi.py entrypoint file 175 | wsgi-file = wsgi.py # the file name 176 | callable = app # the name of the Flask app variable 177 | master = true 178 | processes = 5 # multiple workers for better scale 179 | #http = 0.0.0.0:5000 # we'll not use http anymore instead we use a socket 180 | socket = api.sock 181 | chmod-socket = 660 182 | vacuum = true 183 | die-on-term = true 184 | stats=/tmp/stats.sock # here we can read our stats in realtime 185 | ``` 186 | Instead of exposing uWSGI over HTTP, we want to connect it with an Nginx reverse proxy, which handles all of the outgoing and incoming connections. Therefore we use a Linux Socket `api.sock` that can be shared between our server and the Nginx reverse proxy. 187 | To start the uWSGI app let's create a systemd service file. That allows Linux to automatically run the app. The file needs to be created in **/etc/systemd/system/**. You can find my configuration in **config/api.service**: 188 | ``` 189 | [Unit] 190 | Description=uWSGI instance to serve my api 191 | After=network.target 192 | [Service] 193 | User=cedric # owner of all files 194 | Group=www-data # so that nginx can access the socket 195 | WorkingDirectory=/home/cedric/c-client-flask-server-test # root dir 196 | Environment="PATH=/home/cedric/c-client-flask-server-test/web_test_env/bin" # where uwsgi is installed (virtualenv) 197 | ExecStart=/home/cedric/c-client-flask-server-test/web_test_env/bin/uwsgi --ini config/api.ini # command we want to execute with api.ini file 198 | [Install] 199 | WantedBy=multi-user.target 200 | ``` 201 | The comments above explain the most important options. If you want to know more you can look up all the other options.
202 | If you want to use this service file you need to change at least the `User` option and copy the file to the right location: 203 | ``` 204 | sudo cp config/api.service /etc/systemd/system/api.service 205 | ``` 206 | Now start the service and enable (start on boot) it: 207 | ``` 208 | sudo systemctl start api.service 209 | sudo systemctl enable api.service 210 | ``` 211 | With `sudo systemctl status api.service` we can see whether the start was successful or not. 212 | **Note that we cannot reach our server right now because it's no longer exposed to HTTP. The next step will change that.** 213 | 214 | #### Nginx 215 | The uWSGI server is now doing nothing because the socket it listens to is empty. We're going to set up Nginx to forward all requests to the socket from where our application server can handle them. 216 | First, install Nginx and setup firewall rules: 217 | ``` 218 | sudo apt-get install nginx 219 | sudo ufw delete allow 5000 220 | sudo ufw allow 'Nginx Full' 221 | ``` 222 | Now you should see something when requesting `http:///`. 223 | Then disable the default site: 224 | ``` 225 | sudo rm /etc/nginx/sites-enabled/default 226 | sudo systemctl restart nginx 227 | ``` 228 | Create a new file /etc/nginx/sites-available/api with the following content: 229 | ``` 230 | server { 231 | listen 80; # the standard http port 232 | server_name ; # change that 233 | 234 | location / { 235 | include uwsgi_params; 236 | uwsgi_pass unix:/home/cedric/c-client-flask-server-test/src/server/api.sock; 237 | } 238 | } 239 | ``` 240 | When requesting a `location` the request will be forwarded to the socket where uWSGI takes over. 241 | Now we create a soft link from sites-available to sites-enabled: 242 | ``` 243 | sudo ln -s /etc/nginx/sites-available/api /etc/nginx/sites-enabled 244 | sudo systemctl restart nginx 245 | ``` 246 | When requesting `http:///`you should get the same output as before using the Flask server (but without specifying the port number). If you cannot reach your side check the status of the nginx server and see the log: `sudo less /var/log/nginx/error.log` 247 | 248 | ### Create own CA with self-signed certificates 249 | 250 | The server setup is now almost complete. The last step is to establish a secure HTTPS connection. Since we're running the server on our local network we have no public domain name and cannot get a TLS certificate from something like Let's Encrypt but we can self-generate our certificate.
251 | The problem is that web browsers and also our client will try to verify our certificate which will fail. 252 | Normally certificates get created by an independent CA (certificate authority) and are signed with a private key no one knows. After that, the certificate gets issued to the server that requested it. A CA also has a root certificate that can be used to verify every certificate issued by this CA. The root certificate is shared across all devices and browsers. If an HTTPS connection gets established the server's certificate gets validated by the client using the root certificate of an official CA. 253 | 254 | Our problem is that self-generated certificates are not signed by an official CA, so our connection cannot be trusted. The solution will be to create our own local CA and give our client the root certificate. After that, we can issue a certificate to our server using our CA's private key. This certificate can now be validated by the client since it has the root certificate. 255 | 256 | We will store all files related to certificates in **config/cert/** (not sufficient for real production).
257 | First of all, we create a private key: 258 | ``` 259 | sudo openssl genrsa -des3 -out myApiCA.key 2048 260 | ``` 261 | Then the root certificate: 262 | ``` 263 | sudo openssl req -x509 -new -nodes -key myApiCA.key -sha256 -days 365 -out myApiCA.pem 264 | ``` 265 | The CA is 'ready' now. Next, we create a private key for our service: 266 | ``` 267 | sudo openssl genrsa -out .key 2048 268 | ``` 269 | Now we need to create a CSR which is a certificate signing request. Make sure that the Common Name (CN) is . 270 | 271 | ``` 272 | sudo openssl req -new -key .key -out .csr 273 | ``` 274 | The CA can issue a server certificate by using the CSR: 275 | ``` 276 | sudo openssl x509 -req -in mydomain.com.csr -CA myApiCA.pem -CAkey myApiCA.key -CAcreateserial -out .crt -days 365 -sha256 277 | ``` 278 | The output is .crt which is the certificate we can use for the Nginx server. We change the Nginx api file to the following: 279 | 280 | ``` 281 | server { 282 | listen 80 default_server; 283 | server_name ; # change 284 | return 301 https://$request_uri; #change 285 | } 286 | 287 | server { 288 | listen 443 ssl http2; 289 | server_name ; # change 290 | 291 | ssl_certificate /home/cedric/c-client-flask-server-test/config/cert/api.crt; # the api certificate 292 | ssl_certificate_key /home/cedric/c-client-flask-server-test/config/cert/api.key; # the api private key 293 | ssl_protocols TLSv1.2 TLSv1.1 TLSv1; 294 | 295 | location / { 296 | include uwsgi_params; 297 | uwsgi_pass unix:/home/cedric/c-client-flask-server-test/src/server/api.sock; 298 | } 299 | } 300 | ``` 301 | We do multiple things here. The first `server` block redirects all HTTP traffic to HTTPS. The second `server` block is the HTTPS configuration and forwards the traffic to our socket. You may need to change the server names and the certificate and certificate-key names. 302 | 303 | Let's restart the server: 304 | ``` 305 | sudo systemctl restart nginx 306 | ``` 307 | If we now connect to our server it will force us using HTTPS but still gives an error that it cannot check the certificate. This is normal because our browser does not know about our CA but we can install it. 308 | 309 | Firefox is a great browser to test if everything works fine. [Here is a quick tutorial on how to install a root certificate](https://portswigger.net/support/installing-burp-suites-ca-certificate-in-firefox). 310 | 311 | `http:///` should be redirected to `https:///` and should also be secure. 312 | Chrome makes more validation tests so even with the root certificate, it will give us an error but that's not a problem since we don't want to use a browser but a c client instead. 313 | 314 | If Firefox says our connection is secure after installing our root certificate we know everything went fine (you can ignore if it says it does not know the CA). 315 | Now we have a quite secure server for testing clients using HTTPS on our local network. 316 | 317 | ## Write the web client 318 | 319 | Finally, it's time to think about the client! 320 | 321 | ### Curl and Libcurl 322 | 323 | Maybe you used cURL before. It's pre-installed on most Linux distros today. cURL implements many different protocols such as FTPS, HTTP, HTTPS, IMAPS, etc. It can be used to make requests or transfer data. If you want to know everything about it see [here](https://curl.haxx.se/). 324 | We can also make a cURL request to our service: 325 | ``` 326 | curl http:/// 327 | ``` 328 | This will result in a `301 Moved Permanently` response because we'll not be redirected to https automatically. 329 | 330 | So what happens if we try is using https: 331 | ``` 332 | curl http:/// 333 | ``` 334 | Now we're getting something like `curl: (60) SSL certificate problem: unable to get local issuer certificate` because cURL doesn't know our CA certificate. We can add the `-k` option so that the certificate won't be checked but that is not what we want since we already have our own certificate. So let's fix that: 335 | ``` 336 | curl https:/// --cacert myApiCA.pem 337 | ``` 338 | This time, everything went fine and we got a response: `{"req_method":"GET","req_text":""}`. 339 | 340 | ![](https://user-images.githubusercontent.com/25117793/90991517-0c319b00-e5aa-11ea-9459-12daa420fbd3.png) 341 | 342 | As you see cURL is a great and easy tool but how can we write our own client in C. There are a few libraries out there that allow us to use HTTPS like OpenSSL. The downside is OpenSSL can be really complicated because it's a low-level library that needs a lot of boilerplate code in order to work. Luckily there is a great alternative - `libcurl`! It's a programming API that works the same as cURL. Libcurl can also get complex but in this tutorial, we'll just use the single-threaded and synchronous `easy` interface. 343 | 344 | ### Implement the client 345 | 346 | We can install libcurl using: 347 | ``` 348 | sudo apt install libcurl4-openssl-dev 349 | ``` 350 | In this example, I just want to send an arbitrary GET request to the server and display the response: `./client ` 351 | We have to make some imports first. 352 | ``` c 353 | #include 354 | #include 355 | #include 356 | #include 357 | ``` 358 | Let's define the domain/hostname of our service we want to talk to. In my case, that's `https://api.fritz.box`. 359 | ``` c 360 | const char domain[] = "https://api.fritz.box/"; 361 | ``` 362 | Now, we can enter the main method `int main(int argc, char *argv[]){`: 363 | ``` c 364 | if(argc != 2) return 1; // check if argument count matches 365 | unsigned url_len = strlen(domain) + strlen(argv[1]); // get full length of url 366 | char *url; 367 | 368 | if(!(url = malloc(url_len + 1))) return 1; // allocate memory for the full url 369 | strcpy(url,domain); // copy domain/hostname to url 370 | strcat(url,argv[1]); // concat the path to the url 371 | url[url_len] = '\0'; // termination character 372 | 373 | printf("\n\rrequest: \t%s\n\rresponse: \t", url); // print the request 374 | ``` 375 | After the request is printed out we can send a request and wait for the response: 376 | ``` c 377 | curl_global_init(CURL_GLOBAL_ALL); 378 | CURL *curl = curl_easy_init(); 379 | 380 | if(curl) { // always check for NULL 381 | CURLcode res; 382 | curl_easy_setopt(curl, CURLOPT_URL, url); 383 | curl_easy_setopt(curl, CURLOPT_CAINFO, "/home/progfix/myApiCA.pem"); 384 | 385 | res = curl_easy_perform(curl); 386 | if(res != CURLE_OK) 387 | fprintf(stderr, "error: %s\n", curl_easy_strerror(res)); 388 | 389 | curl_easy_cleanup(curl); 390 | } 391 | curl_global_cleanup(); 392 | free(url); 393 | ``` 394 | `curl_global_init` and `curl_easy_init();` will initialize a new curl context. You always need to call these methods before using the libcurl easy-interface. 395 | `CURLcode res` will be our response code which is `CURLE_OK` 396 | or 0 if the request was successful. 397 | Then, some important options are made. We set the request URL so curl knows the recipient. The next option is really important. Since we use self-signed certificates we have to say the library where the root cert is located so that curl can validate our service. Otherwise, we'll get an error. With `CURLOPT_CAINFO` we set this option. If you use a certificate that's issued by an official CA you don't need this option. Curl will find the CA's root certificate on your computer like any browser does. 398 | 399 | Now it's time to send the request. `res = curl_easy_perform(curl);` will perform our request and set res according to the response. After that, we can check the result and output corresponding errors with `curl_easy_strerror(res)`. 400 | 401 | Curl will write the response directly to `stdout`, so we don't need to print anything now. Most of the time we want to store the output instead of printing it so we need to use other options like [CURLOPT_WRITEFUNCTION](https://curl.haxx.se/libcurl/c/CURLOPT_WRITEFUNCTION.html). 402 | 403 | Finally don't forget to free allocated memory: 404 | ``` 405 | curl_easy_cleanup(curl); 406 | curl_global_cleanup(); 407 | free(url); 408 | ``` 409 | To compile the code run `gcc src/client/client.c -o client -lcurl`. Now we can run the client: `./client hello-world!`. 410 | 411 | ![](https://user-images.githubusercontent.com/25117793/91330669-0dd9a980-e7ca-11ea-8a16-2491a7153603.png) 412 | 413 | **Now you should have a foundation of how to implement your own client-server application using C and Python. Enjoy!** 414 | 415 | -------------------------------------------------------------------------------- /config/api: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | 4 | server_name api.fritz.box; 5 | return 301 https://api.fritz.box$request_uri; 6 | } 7 | 8 | server { 9 | listen 443 ssl http2; 10 | server_name api.fritz.box; 11 | 12 | ssl_certificate /home/cedric/c-client-flask-server-test/config/cert/api.crt; 13 | ssl_certificate_key /home/cedric/c-client-flask-server-test/config/cert/api.key; 14 | 15 | ssl_protocols TLSv1.2 TLSv1.1 TLSv1; 16 | 17 | location / { 18 | include uwsgi_params; 19 | uwsgi_pass unix:/home/cedric/c-client-flask-server-test/src/server/api.sock; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/api.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | chdir = /home/cedric/c-client-flask-server-test/src/server/ 3 | wsgi-file = wsgi.py 4 | callable = app 5 | #module = wsgi:app 6 | # uwsgi --socket 0.0.0.0:5000 --protocol=http --chdir src/server/ -w wsgi:app 7 | 8 | master = true 9 | processes = 5 10 | 11 | #http = 0.0.0.0:5000 12 | socket = api.sock 13 | chmod-socket = 660 14 | vacuum = true 15 | 16 | die-on-term = true 17 | 18 | stats=/tmp/stats.sock 19 | 20 | -------------------------------------------------------------------------------- /config/api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=uWSGI instance to serve my api 3 | After=network.target 4 | 5 | [Service] 6 | User=cedric 7 | Group=www-data 8 | WorkingDirectory=/home/cedric/c-client-flask-server-test 9 | Environment="PATH=/home/cedric/c-client-flask-server-test/web_test_env/bin" 10 | ExecStart=/home/cedric/c-client-flask-server-test/web_test_env/bin/uwsgi --ini config/api.ini 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /config/cert/.gitignore: -------------------------------------------------------------------------------- 1 | api.crt 2 | api.csr 3 | api.key 4 | myApiCA.key 5 | myApiCA.pem 6 | myApiCA.srl 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.2 2 | Flask==1.1.2 3 | itsdangerous==1.1.0 4 | Jinja2==2.11.2 5 | MarkupSafe==1.1.1 6 | uWSGI==2.0.19.1 7 | uwsgitop==0.11 8 | Werkzeug==1.0.1 9 | -------------------------------------------------------------------------------- /src/client/.gitignore: -------------------------------------------------------------------------------- 1 | client 2 | -------------------------------------------------------------------------------- /src/client/client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | const char domain[] = "https://api.fritz.box/"; 7 | 8 | int main(int argc, char *argv[]){ 9 | 10 | if(argc != 2) return 1; 11 | unsigned url_len = strlen(domain) + strlen(argv[1]); 12 | char *url; 13 | 14 | if(!(url = malloc(url_len + 1))) return 1; 15 | strcpy(url,domain); 16 | strcat(url,argv[1]); 17 | url[url_len] = '\0'; 18 | 19 | printf("\n\rrequest: \t%s\n\rresponse: \t", url); 20 | 21 | curl_global_init(CURL_GLOBAL_ALL); 22 | 23 | CURL *curl = curl_easy_init(); 24 | if(curl) { 25 | 26 | CURLcode res; 27 | curl_easy_setopt(curl, CURLOPT_URL, url); 28 | curl_easy_setopt(curl, CURLOPT_CAINFO, "/home/progfix/myApiCA.pem"); 29 | 30 | res = curl_easy_perform(curl); 31 | if(res != CURLE_OK) 32 | fprintf(stderr, "error: %s\n", curl_easy_strerror(res)); 33 | 34 | curl_easy_cleanup(curl); 35 | } 36 | curl_global_cleanup(); 37 | free(url); 38 | 39 | return 0; 40 | } 41 | -------------------------------------------------------------------------------- /src/client/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | clear 3 | sleep 2 4 | x=0 5 | while [ $x -le 1000 ] 6 | do 7 | ./client "hello-world-$x" 8 | x=$(( $x + 1 )) 9 | sleep 0.3 10 | done 11 | 12 | -------------------------------------------------------------------------------- /src/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CedricFauth/c-client-flask-server-test/bdbd5403e9bf0a5c43db6d1e60bf86441e90d8d0/src/server/__init__.py -------------------------------------------------------------------------------- /src/server/testserver.py: -------------------------------------------------------------------------------- 1 | # WARNING: This is a development server. 2 | # Do not use it in a production deployment. 3 | 4 | from flask import Flask 5 | from flask import request 6 | from flask import jsonify 7 | from markupsafe import escape 8 | 9 | app = Flask(__name__) 10 | 11 | @app.route('/',methods=['GET','POST']) 12 | def hello_world(yourtext): 13 | return jsonify(req_text=escape(yourtext), req_method=request.method) 14 | 15 | if __name__ == '__main__': 16 | app.run(host='0.0.0.0',debug=False) 17 | -------------------------------------------------------------------------------- /src/server/wsgi.py: -------------------------------------------------------------------------------- 1 | from testserver import app 2 | 3 | if __name__ == '__main__': 4 | app.run() 5 | 6 | --------------------------------------------------------------------------------