TRB140. Accepting POST requests from internet

I have already installed python on TRB140.
TRB140 mobile port IP adress is public static (ISP provider - Tele2) and WebUI at this addres is accessible.
How to create python .py file (text editor on TRB140) and where to put it (I mean directory on TRB140 file system)?
How to accept HTTP POST requests from internet browser?
Simplified schema of my task:
INTERNET <——POST—–>TRB140 ——- SMS ———>Phones
POST request fields:

  1. List of phone numbers;
  2. SMS message.

Task of TRB140 Python is:

  1. to listen mobile WAN port;
  2. accept incoming POST and process it;
  3. send SMS message to inlisted phones.

I have good experience with Apache server and PHP. However with Python I am familiar only (know sintax and some modules).

Thanks in advance for help.

Greetings, @Valerijonas ,

Welcome to Teltonika Community!

Could you please clarify the firmware version that is used on your device?

Also, please note, that custom script writing is outside our technical support scope.

Kind regards,
V.

Hello Vilius
The version is TRB1_R_00.07.20.3.
Of course, writting scripts is my responsibility and I need no support. However as concerns python relationship to Teltonika products I expect support.
I think, my a couple of questions are exactly such ones.
I use AI to find answers, however WebUI menu of the last version of TRB140 differs from old one, but AI is making references to old menu which not exists in a new version.

Regards
Valerijonas

hi,

you can put the script pretty much wherever you want, just not /tmp because that is ram, i personally just created a directory in /home

for creating and editing the file, use vim editor
To create/edit a file: vi /home/myscript.py
Vi basics:

  • Press i to enter “Insert Mode” (to start typing).

  • Press Esc to exit Insert Mode.

  • Type :wq then Enter to save and quit.

  • Type :q! then Enter to quit without saving if you make a mistake.

use the http.server and socketserver (i believe thats what its called) modules

sending the message command is gsmctl -S -s “number text”

i forgot to mention, add a line like this python3 /home/myscript.py & to the /etc/rc.local file for it to run at start of device booting

you will probably need a traffic rule to allow connections to the listening port probably too

Hello

Thanks a lot

I have tested http.server on PC - it woks fine. However due to poor security it is not recommended for production. I don’t know jet what to use for production.

Of course I need a traffic rule, because request to WAN IP address will be directed to TRB140 point for responding of configuration WebUI.

For example, using Apache and PHP on PC it is very flexible - I can request URL with different PHP scrip files, using different URL parameters in GET request or using different payloads in POST requests.

So I need a POST request (from client side usually I use JavaScript fetch() API) rule for directing request to my Python server instead of WebUI.

Best regards

Valerijonas

Greetings, @Valerijonas ,

Thank you for your message,

Since the device runs RutOS (OpenWrt-based Linux), you can run a small Python service on the router that listens for HTTP POST requests and then sends SMS messages using the built-in gsmctl utility.

Typical workflow:

  1. Place your Python script on the device, for example:
/root/scripts/sms_gateway.py
  1. In the script, run a small HTTP server (e.g., on port 8080) that accepts POST requests.
  2. Parse the POST payload to extract the phone numbers and message text.
  3. Use the command below from the script to send SMS messages:
gsmctl -S <phone_number> "message"

Example request format:

POST http://<device_ip>:8080
numbers=+37060000001,+37060000002&message=Test message

You can find a list of supported python modules here:

Firewall configuration is also required so the router accepts incoming requests from the internet:

WebUI method:

  • Go to Network → Firewall → Traffic Rules
  • Click Add
  • Configure:
    • Name: allow_post_api (or similar)
    • Source zone: WAN
    • Destination zone: Device
    • Protocol: TCP
    • Destination port: 8080 (or the port used by your script)
    • Action: ACCEPT
  • Save and Apply.

After that, external systems should be able to reach the HTTP endpoint.

Best regards,
V.

Hello

Thank Vilius for detailed explanation, I even not need so detailed.
I just before Vilius post already had tested on TRB140 this, as Vilius have said, small python servise (or rather module) - http.server and also tested sending SMS to mobile phones. Everything works OK. I assumed the distinguishing between WebUi and client web server is by using different ports and I was right - for WebUi port is 443 and for testing client server I used 9000 port.

However the important questions are not cleared jet.

1. The Python module http.server and based on it wsgiref.simple_server is not recommended for production due to week security reasons. So it’s not clear jet what to use for production. I tried to install good server framework - sanic. However for installation it requires to install python pip. Both pip and sanic require more flash memory then it is available on trb140, so installation was impossible. What could somebody recommend?

2. It would be better and I would like insistingly to use HTTPS instead of HTTP server. For this is required SSL certificate issued of trusted authority like LetsEncrypt, Google or others. Existing certificates on TRB140 is issued by Teltonika, which is not trusted authority and client services (internet browsers for example) give security warnings. Of course I can change them, however the problem is haw to automatically renew certificate (.crt or .pem files). On a PC I use acme.sh or CertBot for Linux and WIN-ACME for Windows. What to use for TRB140 and how?

Kind regards
Valerijonas

Hello,

Regarding the SSL/TLS certificate setup for your Teltonika TRB140 , here’s a practical approach that ensures trusted HTTPS without browser warnings:

Key Points

  1. Goal: Replace the default Teltonika self-signed certificate with one from a trusted authority (e.g., Let’s Encrypt) and ensure it renews automatically.
  2. Options for TRB140:
  • Run acme.sh directly on the router: Possible, but fragile due to limited storage, firmware quirks, and cron reliability.
  • Run ACME client externally (recommended): Use your existing ACME setup on a server or PC (acme.sh, Certbot, win-acme) and push renewed certificates to the router automatically.

Recommended Approach (External Automation)

  1. Issue/renew certificate externally using your usual ACME client:
acme.sh --issue --dns ... -d device.yourdomain.com
  1. Deploy to TRB140 automatically :
scp fullchain.pem root@TRB140_IP:/etc/uhttpd.crt
scp privkey.pem root@TRB140_IP:/etc/uhttpd.key
ssh root@TRB140_IP "/etc/init.d/uhttpd restart"
  1. Full automation: acme.sh can run the above deploy/reload commands automatically after each renewal.

Why this method is best

  • Works reliably behind LTE/CGNAT networks
  • Avoids router storage and package limitations
  • Fully automated renewal with zero manual steps
  • Uses trusted certificates, eliminating browser security warnings

Optional Router-Side Method

  • Using acme.sh on the TRB140 is possible but requires:
    • DNS validation
    • Persistent storage for certificates
    • Manual cron verification
  • Less robust than external automation

For production use, generate and renew the certificate externally and deploy it to the TRB140. This ensures secure, trusted HTTPS with fully automated updates.

Best regards,
V.

GOOD NEW. The desired task is successfully and completely solved!

Because I see big interest - many views of this discussion - I decided to deal how I have implemented this task.
Let’s remember the task:

  1. TRB140 (or any other similar device) shell receive HTTPS POST request from external source trough TRB140 WAN port, process it and send SMS message to the phones included in a POST request. SMS message shell be inside the POST request too.

  2. Communication with user defined server inside TRB140, also WebUI and CLI must use SSL certificate from trusted provider (letsEncrypt for example) with automatic renewal.

By default from factory inside TRB140 are installed Teltonika self-signed certificates without automatic renewal. Teltonika is not a trusted provider. Therefore, when WebUi or CLI is accessed remotely through WAN port, browsers still use HTTPS (SSL) communication, however with red notification “Not secure”. CURL request from servers operate OK without any error messages.

The first part of task without encrypted SSL/TSL using HTTP POST request was implemented and tested very quickly and easily using Python HTTPServer and BaseHTTPRequestHandler from Python standard http module. The only challenge was compatibility between Python versions, because Teltonika offers for instalattion an old 3.9 version.

As far as concerns the second part of task - there was really big challenge.

Because TRB140 operating system is Linux based, for SSL certificates was made decision to use asme.sh bash script as it is simple and small.

Let’s for clarification at first parse the Vilius from Teltonika last post with advises for solution.

Key Points

2. Options for TRB140:
• Run acme.sh directly on the router: Possible, but fragile due to limited storage, firmware quirks, and cron reliability.

What does it mean limited storage in this situation? This is meaningless, because installed asme.sh occupies just several percent of free storage.
What does it mean firmware quirks? Of course firmware is not powerful and implementation of task shell be tested - however if tests is OK it means the firmware is operational.
What does it mean cron reliability? Crontab records can be tested too. Using automatic renewal there is only one simple crontab record and it should work. If Teltonika produces not reliable product, this product for serious application can not be used at all. Isn’t it?

• Run ACME client externally (recommended): Use your existing ACME setup on a server or PC (acme.sh, Certbot, win-acme) and push renewed certificates to the router automatically.

Absolutely inaccessible advice. TRB140 operating would be dependent on the external devices. If external devices would fail, TRB140 can fail too. TRB140 or similar device should insure independent operating at any time.

Recommended Approach (External Automation)

1. Issue/renew certificate externally using your usual ACME client:


acme.sh --issue --dns … -d device.yourdomain.com

This so called dns method is completely manual, because for verification it requires to add TXT type DNS record with randomly generated text from certificate provider and after certificate is issued to remove it. This procedure is manual and shell be done on first certificate issue and on subsequent renewals.
Other methods such as webroot or standalone require access to domains root directory www for writing. TRB140 /www directory is firmware one and therefore read only, so methods wouldn’t work at all.

2. Deploy to TRB140 automatically :


scp fullchain.pem root@TRB140_IP:/etc/uhttpd.crt
scp privkey.pem root@TRB140_IP:/etc/uhttpd.key
ssh root@TRB140_IP “/etc/init.d/uhttpd restart”

This is Unix/Linux bash commands. What to do if using other systems - Windows, Android, etc.? And again - dependency from external devices!

3. Full automation: acme.sh can run the above deploy/reload commands automatically after each renewal.

Why this method is best

Works reliably behind LTE/CGNAT networks
Avoids router storage and package limitations
Fully automated renewal with zero manual steps
Uses trusted certificates, eliminating browser security warnings

Of course it’s clear now that this “best method” it is not a truth .

MY IMPLEMENTATION OF API FOR THE MENTIONED TASK

New key points

Using asme.sh bash script here still is one method so called DNS-API suitable for automatic renewal.
Verification of this method is also based on creating DNS TXT records, however DNS provider mast have implemented API with automatic appending and removing of such record.
In Lithuania no one provider has such API.
Below is a worldwide list of other DNS providers which support DNS-API method:

Task was implemented by this method choosing Cloudflare DNS provider.

Step by step explanation

1. DNS records concerning TRB140 from local provide I have exported to Cloudflare.
2. From GitHub to Windows PC was downloaded acme.sh zipped package and unzipped.
3. In root@TRB140:/tmp/#directory was created temporary directory: root@TRB140:/tmp/# mkdir acme.
4. Using Windows WinSCP acme package was copied to root@TRB140:/tmp/acme# directory.
5. In the root@TRB140:/etc# directory was created root@TRB140:/etc# mkdir acme directory for installing acme.sh.
6. From root@TRB140:/etc/acme# directory was made acme.sh installation:

 root@TRB140:/etc/acme# acme.sh --home /etc/acme --accountemail "<your email for acme messaging>"

--home /etc/acme: Sets the installation and configuration directory.
--accountemail: Required for registration with the Certificate Authority (like Let’s Encrypt).

7. From root@TRB140:/tmp# removed temporary root@TRB140:/tmp# rm -rf acme directory.
8. Changed directory to root@TRB140:/etc/acme#
9. Made acme.sh executable: root@TRB140:/etc/acme# chmod +x /etc/acme/acme.sh
10. To use the command simply typing as acme.sh from anywhere to file root@TRB140:/etc/profile should be added alias

root@TRB140:/etc/acme# echo "alias acme.sh='/etc/acme/acme.sh'" >> /etc/profile
root@TRB140:/etc/acme# source /etc/profile

because profile file is executed on TRB140 rebooting so it ensures keeping alias persistent.

11. For using Cloudflair DNS API the acme.sh (or rather LetsEncrypt) must know how to access this API.
For that using internal vi editor to root@TRB140:/etc/profile file manually by root@TRB140:/etc# vi profile must be added persistent variables:

...
export ENV=/etc/shinit #existing one
export CF_Key="47f9ce558acda0a54559678c60e1e007c4cac" #new added
export CF_Email="<email in Claudflair account>" #new added
case "$TERM" in #existing one
...

Please notice that in the internet you can find following advice to execute bash commands:

bash
export CF_Key="your_global_api_key"
export CF_Email="your email in Claudflair account"

In our case it is wrong, because a life time of variables is until rebooting. After TRB140 rebooting these variables will no longer exist.
So for persistence variables must be included in the root@TRB140:/etc/profile file.
Here:
CF_Keyis aGlobal API Key value copied from Cloudflair account profile>API Tokens.
Do not confuse with Origin CA Key - must be Global API Key!
CF_Emailis email from Cloudflair accountprofile>Settings.

12. For certificate generation mast be used following bash command:

root@TRB140:/...#acme.sh --issue --dns <dns_provider_name> -d <TRB140 dns name>

<dns_provider_name> for Cloudflair is dns_cf., so replace <dns_provider_name> with dns-cf.

13. Check if in the cron job for automatic renewal is created:

root@TRB140:/...#crontab -l

On TRB140 CLI you have to see the bash response:

59 7 * * * "/etc/acme"/acme.sh --cron --home "/etc/acme" > /dev/null

This string is added automatically by acme.ch to file root@TRB140:/etc/crontabs/root .
You can include this string manually too: root@TRB140:/…# crontab -e .
This bash command invokes vi editor of root file.

14. Now you shall replace self-signed Teltonika files with already generated from trusted DNS provider.
However before this action it is recommended to backup Teltonika files:

cp /etc/uhttpd.crt /etc/uhttpd.crt.bak
cp /etc/uhttpd.key /etc/uhttpd.key.bak

Replacing files by bash command:

root@TRB140:/...#acme.sh --install-cert -d <TRB140 domain name> \
--cert-file /etc/uhttpd.crt \
--key-file /etc/uhttpd.key \
--fullchain-file /etc/uhttpd.crt \
--reloadcmd "/etc/init.d/uhttpd restart"

Is it all? Not. Now TRB140 WebUI will be accessible by domain name, however command line interface CLI not accessible jet. Why?
When in Cloudflair A type DNS records was imported or created, Cloudflair proxy server is enabled automatically. So all traffic goes trough proxy:

[Internet client]↔[Cloudflair proxy]↔[TRB140] .

However Cloudflair proxy does not support SSH on 22 port, which is required for communication with CLI.
So you need again to login in CloudFlair and disable proxy. After disabling proxy communication will be direct and CLI become accessible:

[Internet client]↔[TRB140] .

Cons and pros of direct communication
Cons. Using domain access TRB140 WAN IP address becomes visible throughout the world.
Pros. TRB140 inside is proof from attackers, because WebUI and CLI server root directory /www belongs to so called read-only file system (firmware). However from intensive automatically generated request attacks TRB140 is not protected.

Python server for POST request reception and SMS sending

At first for communication it should be assigned free port, because SSL port 443 is used by TRB140 WebUI. Let’s say it can be 8443.
So request URL should be https://<TRB 140 domain name>:8443. Using non typical port also protects from majority of attacks, because almost all attackers use standard ports.
Using POST requests only also has protective effect, because they can be send from client side JavaScript or CURL requests from servers only. From address bar of browsers POST requests can’t be send.

Post request from client JavaScript:


'use strict';
const array[<your array structure with SMS messages and phone numbers>],
Body=JSON.stringify(array),
URL="<TRB140 domen name>:8443",
Post=async()=>{
  <your code here>
  let response=await fetch(URL,{method:'POST',headers:{'Accept':'application/json','Content-Type':'application/json'},body:Body});
  if(response.ok){<your code here>}
  <your code here>
};
Post();


Python HTTPS server on TRB140 (.py file):


from http.server import BaseHTTPRequestHandler,HTTPServer
from urllib.parse import urlsplit,parse_qsl
import logging,json,os,ssl
class S(BaseHTTPRequestHandler):
  def _set_response(self):
    self.send_response(200)
    self.send_header('Access-Control-Allow-Origin','*')
    self.end_headers()
  def do_OPTIONS(self):
    self.send_response(200)
    self.send_header('Access-Control-Allow-Origin','*')
    self.send_header('Access-Control-Allow-Headers','Content-Type')
    self.end_headers()
  def log_request(self, code='-', size='-'):
    if code != 200:
      super().log_request(code, size)
  def do_POST(self):
    p_data=self.rfile.read(int(self.headers['Content-Length']))
    self._set_response()
    v1=json.loads(p_data)
    for x1 in v1:
      for x2 in x1[1]:
        os.system('gsmctl -S -s "'+x2+x1[0]+'" > /dev/null 2>&1')
    self.wfile.write(b'')
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
cert_path = '/etc/uhttpd.crt'
key_path = '/etc/uhttpd.key'
context.load_cert_chain(certfile=cert_path, keyfile=key_path)
httpd=HTTPServer(('<TRB140 WAN IP>',8443),S)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()

For normal without errors POST requests from browsers (tested with several most popular browsers) the following conditions should be met:

  1. Content security policy CSP must be set to:
    “default-src ‘self’ https://:8443”.
  2. As you can see in the Python code above:
    2.1. In the function _set_response(self) must be included self.send_header(‘Access-Control-Allow-Origin’, ‘*’) ;
    2.2. Must be included function:
def do_OPTIONS(self):
    self.send_response(200)
    self.send_header('Access-Control-Allow-Origin','*')
    self.send_header('Access-Control-Allow-Headers','Content-Type')
    self.end_headers()

2.3. In the function do_POST(self) must be included self.wfile.write(b’') , which sends empty feedback to the browser.
Without fulfilling all mentioned conditions browser gives errors and not work.

The main disadvantage is still security, because everyone can implement direct access from browsers.

So much better is making request from single proxy server using CURL POST requests.
All clients shell send POST requests to the proxy.
On a TRB140 can be set firewall rule to accept requests only from single proxy WAN IP. So for all attackers direct access will be impossible.
Also all mentioned conditions required for direct accessing from client is not required for accessing from proxy.
Python server code becomes much simple and not accessible directly from client.


from http.server import BaseHTTPRequestHandler,HTTPServer
from urllib.parse import urlsplit,parse_qsl
import logging,json,os,ssl
class S(BaseHTTPRequestHandler):
  def _set_response(self):
    self.send_response(200)
    self.flush_headers()
  def log_request(self, code='-', size='-'):
    if code != 200:
      super().log_request(code, size)
  def do_POST(self):
    p_data=self.rfile.read(int(self.headers['Content-Length']))
    self._set_response()
    v1=json.loads(p_data)
    for x1 in v1:
      for x2 in x1[1]:
        os.system('gsmctl -S -s "'+x2+x1[0]+'" > /dev/null 2>&1')
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
cert_path = '/etc/uhttpd.crt'
key_path = '/etc/uhttpd.key'
context.load_cert_chain(certfile=cert_path, keyfile=key_path)
httpd=HTTPServer(('<TRB140 WAN IP>',8443),S)
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
httpd.serve_forever()

Please notice that in the function _set_response(self) is used self.flush_headers() instead of self.end_headers() .
This works normally from proxy, however do not work from browsers - again better for security.

Below is a PHP code of proxy server on Apache platform:


<?php
if($_SERVER['REQUEST_METHOD']!=='POST'){http_response_code(403);exit('ERROR 403: PROHIBITED!');}
$payload=file_get_contents('php://input');
$data=null;$ch=curl_init();
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch,CURLOPT_HTTPHEADER,['Content-Type:application/json']);
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch,CURLOPT_URL,'https://<TRB140 domain>:8443'); 
curl_setopt($ch,CURLOPT_POSTFIELDS,$payload);
curl_exec($ch);
$errstr=null;
if(curl_errno($ch)){$errstr=curl_errno($ch).": ".curl_error($ch);}
if(isset($errstr))exit($errstr);
echo "OK";
?>

Notice that this code accepts only POST requests. For other requests code returns: ‘ERROR 403: PROHIBITED!’. This is security element too.
In this way we have communication traffic:

[Internet clients]↔[Proxy server]↔[TRB140 Python server]

As an example a JavaScript code of an internet client:


'use strict';
const
Post=async(rq)=>{
	try{let r=await fetch(rq[0],{method:'POST',headers:{'Content-Type':'application/json'},body:rq[1]})
		if(r.ok){
			r=await r.text();
			document.body.textContent=r; //or any other your code
		}else console.log(r.status);
	}
	catch(err){console.log(err)}
},
arr1=[<your array structure with SMS messages and phone numbers>];
Post(['<Proxy server URL>',JSON.stringify(arr1)]);

The implemented task with this API was tested with many SMS messages to single phone and many SMS messages to many phones.
Everything operates OK with no errors.
If required the communication can be additionally protected by one or double authentications: passwords (or 2FA, or others methods) from client to proxy and from proxy to TRB140 python server.

I HOPE THIS POST WILL HELP FOR SOMEBODY

Greetings, @Valerijonas,

I hope you’re doing well.

Thank you for sharing your solution with the community. We truly appreciate you taking the time to provide your insights and describe what worked in your case.

Contributions like yours are very valuable and can be helpful to other users who may encounter similar issues in the future.

If you have any further observations or additional tips, feel free to share them.

Best regards,
V.