├── .gitignore ├── README ├── README.md ├── about.py ├── main.py ├── mikrotik_logo.gif ├── printtemp.jpg ├── routeros_api.py ├── rsc2csv.py ├── samplevoucher.png ├── screenshot01.png ├── screenshot02.png └── verbose.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | vouchers.pdf 107 | users.rsc 108 | users.csv 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | # Pyre type checker 119 | .pyre/ 120 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/README -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vougen 2 | The application for to create Mikrotik hotspot user & voucher . Built on python3 and tkinter. 3 | 4 | You maybe like this (Raspberry Pi Version) too https://github.com/scriptik/vougenpi 5 | 6 | With this application you can create the Hotspot users and save it as the .rsc file or write 7 | directly on the Mikrotik Router .Also it create the voucher for you with the QR code as this. 8 | 9 | ![samplevoucher](samplevoucher.png) 10 | 11 | # Running 12 | Simply run python main.py 13 | in the main menu fill the fileds as the below sample 14 | 15 | ![screenshot02](screenshot02.png) 16 | 17 | if the router is not avilable now you can save the out put in the .rsc file and don't 18 | select Write on router checkbox. 19 | 20 | ![screenshot01](screenshot01.png) 21 | 22 | # Connecting to Router 23 | For api-ssl connection , Before connecting, api-ssl service on routeros must have a valid certificate set. For more information on 24 | how to generate such certificates see : 25 | [MikroTik wiki](https://wiki.mikrotik.com/wiki/Manual:Create_Certificates) 26 | # ToDo-List 27 | - [ ] Add api-ssl connection button and active it for new API 28 | 29 | # Built with 30 | ![RouterOS_API](https://github.com/LaiArturs/RouterOS_API) Python API client for RouterOS that is easy to use and modify. 31 | 32 | ![python-qrcode](https://github.com/lincolnloop/python-qrcode) Pure python QR Code generator 33 | 34 | ![reportlab](https://github.com/Distrotech/reportlab) ReportLab PDF Toolkit 35 | -------------------------------------------------------------------------------- /about.py: -------------------------------------------------------------------------------- 1 | #### Imports #### 2 | import tkinter as tk 3 | from tkinter import * 4 | import subprocess 5 | import ast 6 | import os 7 | 8 | #### Initialize tkinter 9 | root = Tk() 10 | root.title("About") 11 | root.geometry("+300+150") 12 | mainWindow = Frame(root) 13 | mainWindow.grid() 14 | 15 | LICENCE = """MIT License 16 | 17 | Copyright (c) 2019 scriptik 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE.""" 35 | 36 | def BtnClose(): 37 | root.quit() 38 | 39 | l = Label(mainWindow, text="Developed by: Pezhman Shafigh\nhttps://github.com/scriptik/vougen\nVersion: 2.0\nLicence: {}\n".format(LICENCE)) 40 | l.grid(row="0", column="0", sticky=E+W+N+S) 41 | 42 | btnClose = Button(mainWindow, command=BtnClose, text="Close") 43 | btnClose.grid(row="1", column="0", sticky=E+W+N+S) 44 | 45 | root.mainloop() 46 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # vougen 3 | # This is for to create Mikrotik Hotspot User & the voucher for it. 4 | # It can write the created user(s) directly on the Mikrotik Routers 5 | # Developed by Pezhman Shafigh 6 | # January 2019 7 | 8 | 9 | from tkinter import * 10 | from tkinter import ttk 11 | from tkinter import messagebox 12 | import subprocess 13 | 14 | from reportlab.pdfgen import canvas 15 | from reportlab.pdfbase import pdfmetrics 16 | from reportlab.pdfbase.ttfonts import TTFont 17 | from pathlib import Path 18 | import string 19 | import random 20 | import qrcode 21 | import os 22 | #from librouteros import connect 23 | #import ssl 24 | from routeros_api import Api 25 | import time 26 | import getpass 27 | 28 | 29 | class vougen: 30 | 31 | def __init__(self, master): 32 | 33 | self.master = master 34 | master.title("VOUGEN V2.0") 35 | master.geometry('660x450+250+100') 36 | 37 | #### Initialize Label Frames #### 38 | 39 | self.header_LabelFrame = ttk.LabelFrame(master, height = 50, width = 640) 40 | self.header_LabelFrame.grid(row= 0, column= 0 , columnspan=2, padx= 5, pady= (2,5)) 41 | self.header_LabelFrame.configure(borderwidth= 0) 42 | 43 | self.voucher_LabelFrame = ttk.LabelFrame(master, text="Voucher", height = 300, width = 320) 44 | self.voucher_LabelFrame.grid(row= 1, column= 0 , padx= 5, pady= 2) 45 | self.voucher_LabelFrame.configure(borderwidth= 2) 46 | 47 | self.action_LabelFrame = ttk.LabelFrame(master, text="Actions", height = 300, width = 320) 48 | self.action_LabelFrame.grid(row= 1, column= 1 , padx= 5, pady= 2) 49 | self.action_LabelFrame.configure(borderwidth= 2) 50 | 51 | self.footer_LabelFrame = ttk.LabelFrame(master, height = 50, width = 640) 52 | self.footer_LabelFrame.grid(row= 2, column= 0 , columnspan=2, padx= 5) 53 | self.footer_LabelFrame.configure(borderwidth= 0) 54 | 55 | # Widget header Frame 56 | self.logo = PhotoImage(file = 'mikrotik_logo.gif') 57 | self.logoheader = ttk.Label(self.header_LabelFrame,image = self.logo) 58 | self.logoheader.grid(row= 0 , column= 0 , sticky='w',padx= (0,180), pady= (0,1)) 59 | 60 | self.labelheader = ttk.Label(self.header_LabelFrame, text="Hotspot User & Voucher generator") 61 | self.labelheader.config(font=("Arial Black", 11)) 62 | self.labelheader.grid(row= 0 , column= 1 , sticky='e') 63 | 64 | # Widget Voucher Frame 65 | self.labellenpass = ttk.Label(self.voucher_LabelFrame, text="Password Length :") 66 | self.labellenpass.grid(row= 0 , column= 0 , sticky='w', padx= 2, pady= 2) 67 | 68 | self.lenpass = IntVar() 69 | self.cboxlenpass = ttk.Combobox(self.voucher_LabelFrame, textvariable = self.lenpass) 70 | self.cboxlenpass.set('8') 71 | self.cboxlenpass.grid(row= 0 , column= 1 , sticky='e', padx= 2, pady= 2) 72 | self.cboxlenpass.configure(width="4", values = ('8','9','10','11','12')) 73 | 74 | self.labellenpass = ttk.Label(self.voucher_LabelFrame, text="Number of Users :") 75 | self.labellenpass.grid(row= 1 , column= 0 , sticky='w', padx= 2, pady= 2) 76 | 77 | self.usnu = IntVar() 78 | self.sboxusnu = ttk.Spinbox(self.voucher_LabelFrame, from_ = 4, to = 400, increment = 4, textvariable = self.usnu) 79 | self.sboxusnu.grid(row= 1 , column= 1 , sticky='e', padx= 2, pady= 2) 80 | self.sboxusnu.configure(width="3") 81 | 82 | self.labellenpass = ttk.Label(self.voucher_LabelFrame, text="User name prefix :") 83 | self.labellenpass.grid(row= 2 , column= 0 , sticky='w', padx= (2,40), pady= 2) 84 | 85 | self.entprefix = ttk.Entry(self.voucher_LabelFrame) 86 | self.entprefix.grid(row= 2 , column= 1 , sticky='e', padx= 2, pady= 2) 87 | self.entprefix.configure(width="12") 88 | 89 | self.labelhotnam = ttk.Label(self.voucher_LabelFrame, text="Name of hotspot :") 90 | self.labelhotnam.grid(row= 3 , column= 0 , sticky='w', padx= (2,40), pady= 2) 91 | 92 | self.enthotnam = ttk.Entry(self.voucher_LabelFrame) 93 | self.enthotnam.grid(row= 3 , column= 1 , sticky='e', padx= 2, pady= 2) 94 | self.enthotnam.configure(width="12") 95 | 96 | self.labelhotdns = ttk.Label(self.voucher_LabelFrame, text="hotspot DNS :") 97 | self.labelhotdns.grid(row= 4 , column= 0 , sticky='w', padx= (2,40), pady= 2) 98 | 99 | self.enthotdns = ttk.Entry(self.voucher_LabelFrame) 100 | self.enthotdns.grid(row= 4 , column= 1 , sticky='e', padx= 2, pady= 2) 101 | self.enthotdns.configure(width="12") 102 | 103 | self.labelplt = ttk.Label(self.voucher_LabelFrame, text="Profile limit time Day(s) :") 104 | self.labelplt.grid(row= 5 , column= 0 , sticky='w', padx= 2, pady= 2) 105 | 106 | self.plt = IntVar() 107 | self.cboxplt = ttk.Combobox(self.voucher_LabelFrame, textvariable = self.plt) 108 | self.cboxplt.set('1') 109 | self.cboxplt.grid(row= 5 , column= 1 , sticky='e', padx= 2, pady= 2) 110 | self.cboxplt.configure(width="4", values = ('1','7','30','60','90', '180', '365')) 111 | 112 | # Widget Action Frame 113 | self.wtfi = IntVar() 114 | self.cbwtfi = ttk.Checkbutton(self.action_LabelFrame, variable = self.wtfi,\ 115 | text="Save to file", onvalue = 1, offvalue = 0) 116 | self.cbwtfi.grid(row= 0 , column= 0 , sticky='w',padx= (2,160), pady= 2) 117 | 118 | self.wtru = IntVar() 119 | self.cbwtru = ttk.Checkbutton(self.action_LabelFrame,variable = self.wtru,\ 120 | text="Write on Router", onvalue = 1, offvalue = 0, command=self.active_in) 121 | self.cbwtru.grid(row= 1 , column= 0 , sticky='w',padx= 2, pady= 2) 122 | 123 | self.labelrouuse = ttk.Label(self.action_LabelFrame, text="Router User name :") 124 | self.labelrouuse.grid(row= 2 , column= 0 , sticky='w', padx= 2, pady= 2) 125 | 126 | self.entrouuse = ttk.Entry(self.action_LabelFrame) 127 | self.entrouuse.grid(row= 2 , column= 1 , sticky='e', padx= 2, pady= 2) 128 | self.entrouuse.configure(width="15", state='disabled') 129 | 130 | self.labelroupass = ttk.Label(self.action_LabelFrame, text="Router password :") 131 | self.labelroupass.grid(row= 3 , column= 0 , sticky='w', padx= 2, pady= 2) 132 | 133 | self.entroupass = ttk.Entry(self.action_LabelFrame) 134 | self.entroupass.grid(row= 3 , column= 1 , sticky='e', padx= 2, pady= 2) 135 | self.entroupass.configure(width="15", show = '*', state='disabled') 136 | 137 | self.labelrouip = ttk.Label(self.action_LabelFrame, text="Router ip :") 138 | self.labelrouip.grid(row= 4 , column= 0 , sticky='w', padx= 2, pady= 2) 139 | 140 | self.entrouip = ttk.Entry(self.action_LabelFrame) 141 | self.entrouip.grid(row= 4 , column= 1 , sticky='e', padx= 2, pady= 2) 142 | self.entrouip.insert(END, '192.168.') 143 | self.entrouip.configure(width="15", state='disabled') 144 | 145 | self.labelroupo = ttk.Label(self.action_LabelFrame, text="Router port :") 146 | self.labelroupo.grid(row= 5 , column= 0 , sticky='w', padx= 2, pady= 2) 147 | 148 | self.entroupo = ttk.Entry(self.action_LabelFrame) 149 | self.entroupo.insert(END, '8728') 150 | self.entroupo.grid(row= 5 , column= 1 , sticky='e', padx= 2, pady= 2) 151 | self.entroupo.configure(width="5", state='disabled') 152 | 153 | # Widget footer Frame 154 | 155 | self.textout = Text(self.footer_LabelFrame, width = 82 , height =12) 156 | self.textout.grid(row="0", column="0",padx= (1,0), pady = 1) 157 | 158 | self.btndoit = Button(self.footer_LabelFrame, text="Do it", command=self.Btndoit) 159 | self.btndoit.grid(row="0", column="1",padx= (2,2), sticky='ne') 160 | self.btndoit.configure(width="5", height =3) 161 | 162 | self.btnAbout = Button(self.footer_LabelFrame, text="About", command=self.BtnAbout) 163 | self.btnAbout.grid(row="0", column="1",padx= (2,2), sticky='e') 164 | self.btnAbout.configure(width="5", height =3) 165 | 166 | self.btnQuit = Button(self.footer_LabelFrame, text="Quit", command=self.BtnQuit) 167 | self.btnQuit.grid(row="0", column="1",padx= (2,2), sticky='se') 168 | self.btnQuit.configure(width="5", height =3) 169 | 170 | #self.info = ttk.Label(self.footer_LabelFrame, text="Hotspot User & Voucher generator") 171 | #self.info.grid(row= 1 , column= 0 , pady=(5,0), sticky='w') 172 | 173 | def BtnAbout(self): 174 | pr_about = subprocess.Popen([ 'python3', 'about.py' ]) 175 | 176 | def Btndoit(self): 177 | self.textout.delete('1.0', END) 178 | self.vou_gen() 179 | if self.wtfi.get() == 1: 180 | pr_csv = subprocess.Popen([ 'python3', 'rsc2csv.py' ]) 181 | s= "user.rsc file created!\nuser.csv file created!" 182 | self.textout.insert(END, s) 183 | self.textout.see(END) 184 | 185 | def active_in(self): 186 | if self.wtru.get() == 1: 187 | self.entrouuse.configure(state='enable') 188 | self.entroupass.configure(state='enable') 189 | self.entrouip.configure(state='enable') 190 | self.entroupo.configure(state='enable') 191 | else: 192 | self.entrouuse.configure(state='disable') 193 | self.entroupass.configure(state='disable') 194 | self.entrouip.configure(state='disable') 195 | self.entroupo.configure(state='disable') 196 | 197 | 198 | def BtnQuit(self): 199 | self.master.quit() 200 | 201 | 202 | def vou_gen(self): 203 | 204 | chars=string.ascii_letters + string.digits 205 | wtofi = self.wtfi.get() 206 | wtoru = self.wtru.get() 207 | idnu = int(self.sboxusnu.get()) 208 | prefix = self.entprefix.get() 209 | size = int(self.cboxlenpass.get()) 210 | dnsname = self.enthotdns.get() 211 | plt = self.cboxplt.get() 212 | hotnam = self.enthotnam.get() 213 | ru_po = self.entroupo.get() 214 | ru_us = self.entrouuse.get() 215 | ru_pa = self.entroupass.get() 216 | ru_ip = self.entrouip.get() 217 | 218 | pdfmetrics.registerFont(TTFont('DejaVuSans','DejaVuSans.ttf')) 219 | pdfmetrics.registerFont(TTFont('Arial','ariblk.ttf')) 220 | 221 | qr = qrcode.QRCode( 222 | version=1, 223 | error_correction=qrcode.constants.ERROR_CORRECT_L, 224 | box_size=2, 225 | border=4, 226 | ) 227 | 228 | ############################################ 229 | 230 | filename = 'users.rsc' 231 | if wtofi == 1: 232 | for p in Path(".").glob("users.rsc"): 233 | p.unlink() 234 | with open(filename, 'w') as file_object: 235 | file_object.write("/ip hotspot user\n") 236 | 237 | if wtoru == 1: 238 | print("Connecting to Router...") 239 | 240 | limit_time = plt+"d" 241 | userdic = {} 242 | for i in range(idnu): 243 | user = prefix+str(i) 244 | #passwd = (self.password_generator(size)) 245 | passwd = ''.join(random.choice(chars) for i in range(size)) 246 | userdic.update({user : passwd}) 247 | roucomm = ("add limit-uptime="+limit_time+" name="+user+" password="+passwd+" server="+hotnam) 248 | s = '{}\n'.format(roucomm) 249 | self.textout.insert(END, s) 250 | self.textout.see(END) 251 | if wtofi == 1: 252 | with open(filename, 'a') as file_object: 253 | file_object.write(roucomm+"\n") 254 | if wtoru == 1: 255 | router = Api(ru_ip, user=ru_us, password=ru_pa, port=int(ru_po)) 256 | message = [('/ip/hotspot/user/add', '=name=' + user, '=password=' + passwd, '=server=' + hotnam, \ 257 | '=limit-uptime=' + limit_time)] 258 | r = router.talk(message) 259 | print(r) 260 | 261 | 262 | qr.clear() 263 | qr.add_data('http://'+dnsname+'/login?username='+user+'&password='+passwd) 264 | qr.make(fit=True) 265 | 266 | img = qr.make_image(fill_color="black", back_color="white") 267 | #img.show() 268 | img.save(user+".png") 269 | 270 | 271 | #print("Waiting ......") 272 | ## Create the print temp 273 | ptemp = "printtemp.jpg" 274 | profile = plt+" DAY Access" 275 | pnu = int(idnu/4) 276 | minra = 0 277 | maxra = 4 278 | c = canvas.Canvas("vouchers.pdf") 279 | for p in range(pnu): 280 | c.setLineWidth(.1) 281 | c.setFont('Helvetica', 12) 282 | 283 | c.drawInlineImage(ptemp, 20,0, width=240,height=300) 284 | c.drawInlineImage(ptemp, 320,0, width=240,height=300) 285 | c.drawInlineImage(ptemp, 20,400, width=240,height=300) 286 | c.drawInlineImage(ptemp, 320,400, width=240,height=300) 287 | 288 | 289 | 290 | xqr = {0:160,1:460,2:160,3:460} 291 | yqr = {0:505,1:505,2:105,3:105} 292 | xus ={0:38,1:338,2:38,3:338} 293 | yus ={0:555,1:555,2:155,3:155} 294 | xps ={0:38,1:338,2:38,3:338} 295 | yps ={0:515,1:515,2:115,3:115} 296 | 297 | c.setFont("Arial", 13) 298 | i = 0 299 | for j in range(minra,maxra): 300 | qrimage = prefix+str(j)+".png" 301 | userq = prefix+str(j) 302 | passq = (userdic[userq]) 303 | xqri = (xqr[i]) 304 | yqri = (yqr[i]) 305 | xusi = (xus[i]) 306 | yusi = (yus[i]) 307 | xpsi = (xps[i]) 308 | ypsi = (yps[i]) 309 | c.drawInlineImage(qrimage, xqri,yqri, width=None,height=None) 310 | c.drawString(xusi,yusi, userq) 311 | c.drawString(xpsi,ypsi, passq) 312 | i += 1 313 | minra += 4 314 | maxra += 4 315 | c.setFont("Arial", 10) 316 | c.drawString(37,605, profile) 317 | c.drawString(337,605, profile) 318 | c.drawString(37,205, profile) 319 | c.drawString(337,205, profile) 320 | c.showPage() 321 | c.save() 322 | self.del_temp(prefix) 323 | 324 | 325 | def del_temp(self, prefix): 326 | for p in Path(".").glob(prefix+"*.png"): 327 | p.unlink() 328 | #print("QR temp files Removed!") 329 | 330 | 331 | def main(): 332 | 333 | root = Tk() 334 | app = vougen(root) 335 | root.mainloop() 336 | 337 | if __name__ == "__main__": 338 | main() 339 | -------------------------------------------------------------------------------- /mikrotik_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/mikrotik_logo.gif -------------------------------------------------------------------------------- /printtemp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/printtemp.jpg -------------------------------------------------------------------------------- /routeros_api.py: -------------------------------------------------------------------------------- 1 | # Author: Arturs Laizans 2 | 3 | import socket 4 | import ssl 5 | import hashlib 6 | import binascii 7 | from verbose import Log 8 | 9 | # Constants - Define defaults 10 | PORT = 8728 11 | SSL_PORT = 8729 12 | 13 | USER = 'admin' 14 | PASSWORD = '' 15 | 16 | USE_SSL = False 17 | 18 | VERBOSE = False # Whether to print API conversation width the router. Useful for debugging 19 | VERBOSE_LOGIC = 'OR' # Whether to print and save verbose log to file. AND - print and save, OR - do only one. 20 | VERBOSE_FILE_MODE = 'w' # Weather to create new file ('w') for log or append to old one ('a'). 21 | 22 | CONTEXT = ssl.create_default_context() # It is possible to predefine context for SSL socket 23 | CONTEXT.check_hostname = False 24 | CONTEXT.verify_mode = ssl.CERT_NONE 25 | 26 | 27 | class LoginError(Exception): 28 | pass 29 | 30 | 31 | class WordTooLong(Exception): 32 | pass 33 | 34 | 35 | class CreateSocketError(Exception): 36 | pass 37 | 38 | 39 | class RouterOSTrapError(Exception): 40 | pass 41 | 42 | 43 | class Api: 44 | 45 | def __init__(self, address, user=USER, password=PASSWORD, use_ssl=USE_SSL, port=False, 46 | verbose=VERBOSE, context=CONTEXT): 47 | 48 | self.address = address 49 | self.user = user 50 | self.password = password 51 | self.use_ssl = use_ssl 52 | self.port = port 53 | self.verbose = verbose 54 | self.context = context 55 | 56 | # Port setting logic 57 | if port: 58 | self.port = port 59 | elif use_ssl: 60 | self.port = SSL_PORT 61 | else: 62 | self.port = PORT 63 | 64 | # Create Log instance to save or print verbose logs 65 | self.log = Log(verbose, VERBOSE_LOGIC, VERBOSE_FILE_MODE) 66 | self.log('') 67 | self.log('#-----------------------------------------------#') 68 | self.log('API IP - {}, USER - {}'.format(address, user)) 69 | self.sock = None 70 | self.connection = None 71 | self.open_socket() 72 | self.login() 73 | self.log('Instance of Api created') 74 | self.is_alive() 75 | 76 | # Open socket connection with router and wrap with SSL if needed. 77 | def open_socket(self): 78 | 79 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 80 | # self.sock.settimeout(5) # Set socket timeout to 5 seconds, default is None 81 | 82 | try: 83 | # Trying to connect to RouterOS, error can occur if IP address is not reachable, or API is blocked in 84 | # RouterOS firewall or ip services, or port is wrong. 85 | self.connection = self.sock.connect((self.address, self.port)) 86 | 87 | except OSError: 88 | raise CreateSocketError('Error: API failed to connect to socket. Host: {}, port: {}.'.format(self.address, 89 | self.port)) 90 | 91 | if self.use_ssl: 92 | self.sock = self.context.wrap_socket(self.sock) 93 | 94 | self.log('API socket connection opened.') 95 | 96 | # Login API connection into RouterOS 97 | def login(self): 98 | sentence = ['/login', '=name=' + self.user, '=password=' + self.password] 99 | reply = self.communicate(sentence) 100 | if len(reply[0]) == 1 and reply[0][0] == '!done': 101 | # If login process was successful 102 | self.log('Logged in successfully!') 103 | return reply 104 | elif 'Error' in reply: 105 | # Else if there was some kind of error during login process 106 | self.log('Error in login process - {}'.format(reply)) 107 | raise LoginError('Login ' + reply) 108 | elif len(reply[0]) == 2 and reply[0][1][0:5] == '=ret=': 109 | # Else if RouterOS uses old API login method, code continues with old method 110 | self.log('Using old login process.') 111 | md5 = hashlib.md5(('\x00' + self.password).encode('utf-8')) 112 | md5.update(binascii.unhexlify(reply[0][1][5:])) 113 | sentence = ['/login', '=name=' + self.user, '=response=00' 114 | + binascii.hexlify(md5.digest()).decode('utf-8')] 115 | self.log('Logged in successfully!') 116 | return self.communicate(sentence) 117 | 118 | # Sending data to router and expecting something back 119 | def communicate(self, sentence_to_send): 120 | 121 | # There is specific way of sending word length in RouterOS API. 122 | # See RouterOS API Wiki for more info. 123 | def send_length(w): 124 | length_to_send = len(w) 125 | if length_to_send < 0x80: 126 | num_of_bytes = 1 # For words smaller than 128 127 | elif length_to_send < 0x4000: 128 | length_to_send += 0x8000 129 | num_of_bytes = 2 # For words smaller than 16384 130 | elif length_to_send < 0x200000: 131 | length_to_send += 0xC00000 132 | num_of_bytes = 3 # For words smaller than 2097152 133 | elif length_to_send < 0x10000000: 134 | length_to_send += 0xE0000000 135 | num_of_bytes = 4 # For words smaller than 268435456 136 | elif length_to_send < 0x100000000: 137 | num_of_bytes = 4 # For words smaller than 4294967296 138 | self.sock.sendall(b'\xF0') 139 | else: 140 | raise WordTooLong('Word is too long. Max length of word is 4294967295.') 141 | self.sock.sendall(length_to_send.to_bytes(num_of_bytes, byteorder='big')) 142 | 143 | # Actually I haven't successfully sent words larger than approx. 65520. 144 | # Probably it is some RouterOS limitation of 2^16. 145 | 146 | # The same logic applies for receiving word length from RouterOS side. 147 | # See RouterOS API Wiki for more info. 148 | def receive_length(): 149 | r = self.sock.recv(1) # Receive the first byte of word length 150 | 151 | # If the first byte of word is smaller than 80 (base 16), 152 | # then we already received the whole length and can return it. 153 | # Otherwise if it is larger, then word size is encoded in multiple bytes and we must receive them all to 154 | # get the whole word size. 155 | 156 | if r < b'\x80': 157 | r = int.from_bytes(r, byteorder='big') 158 | elif r < b'\xc0': 159 | r += self.sock.recv(1) 160 | r = int.from_bytes(r, byteorder='big') 161 | r -= 0x8000 162 | elif r < b'\xe0': 163 | r += self.sock.recv(2) 164 | r = int.from_bytes(r, byteorder='big') 165 | r -= 0xC00000 166 | elif r < b'\xf0': 167 | r += self.sock.recv(3) 168 | r = int.from_bytes(r, byteorder='big') 169 | r -= 0xE0000000 170 | elif r == b'\xf0': 171 | r = self.sock.recv(4) 172 | r = int.from_bytes(r, byteorder='big') 173 | 174 | return r 175 | 176 | def read_sentence(): 177 | rcv_sentence = [] # Words will be appended here 178 | rcv_length = receive_length() # Get the size of the word 179 | 180 | while rcv_length != 0: 181 | received = b'' 182 | while rcv_length > len(received): 183 | rec = self.sock.recv(rcv_length - len(received)) 184 | if rec == b'': 185 | raise RuntimeError('socket connection broken') 186 | rec = rec 187 | received += rec 188 | received = received.decode('utf-8') 189 | self.log('<<< {}'.format(received)) 190 | rcv_sentence.append(received) 191 | rcv_length = receive_length() # Get the size of the next word 192 | self.log('') 193 | return rcv_sentence 194 | 195 | # Sending part of conversation 196 | 197 | # Each word must be sent separately. 198 | # First, length of the word must be sent, 199 | # Then, the word itself. 200 | for word in sentence_to_send: 201 | send_length(word) 202 | self.sock.sendall(word.encode('utf-8')) # Sending the word 203 | self.log('>>> {}'.format(word)) 204 | self.sock.sendall(b'\x00') # Send zero length word to mark end of the sentence 205 | self.log('') 206 | 207 | # Receiving part of the conversation 208 | 209 | # Will continue receiving until receives '!done' or some kind of error (!trap). 210 | # Everything will be appended to paragraph variable, and then returned. 211 | paragraph = [] 212 | received_sentence = [''] 213 | while received_sentence[0] != '!done': 214 | received_sentence = read_sentence() 215 | paragraph.append(received_sentence) 216 | return paragraph 217 | 218 | # Initiate a conversation with the router 219 | def talk(self, message): 220 | 221 | # It is possible for message to be string, tuple or list containing multiple strings or tuples 222 | if type(message) == str or type(message) == tuple: 223 | return self.send(message) 224 | elif type(message) == list: 225 | reply = [] 226 | for sentence in message: 227 | reply.append(self.send(sentence)) 228 | return reply 229 | else: 230 | raise TypeError('talk() argument must be str or tuple containing str or list containing str or tuples') 231 | 232 | def send(self, sentence): 233 | # If sentence is string, not tuples of strings, it must be divided in words 234 | if type(sentence) == str: 235 | sentence = sentence.split() 236 | reply = self.communicate(sentence) 237 | 238 | # If RouterOS returns error from command that was sent 239 | if '!trap' in reply[0][0]: 240 | # You can comment following line out if you don't want to raise an error in case of !trap 241 | raise RouterOSTrapError("\nCommand: {}\nReturned an error: {}".format(sentence, reply)) 242 | pass 243 | 244 | # reply is list containing strings with RAW output form API 245 | # nice_reply is a list containing output form API sorted in dictionary for easier use later 246 | nice_reply = [] 247 | for m in range(len(reply) - 1): 248 | nice_reply.append({}) 249 | for k, v in (x[1:].split('=', 1) for x in reply[m][1:]): 250 | nice_reply[m][k] = v 251 | return nice_reply 252 | 253 | def is_alive(self) -> bool: 254 | """Check if socket is alive and router responds""" 255 | 256 | # Check if socket is open in this end 257 | try: 258 | self.sock.settimeout(2) 259 | except OSError: 260 | self.log("Socket is closed.") 261 | return False 262 | 263 | # Check if we can send and receive through socket 264 | try: 265 | self.talk('/system/identity/print') 266 | 267 | except (socket.timeout, IndexError, BrokenPipeError): 268 | self.log("Router does not respond, closing socket.") 269 | self.close() 270 | return False 271 | 272 | self.log("Socket is open, router responds.") 273 | self.sock.settimeout(None) 274 | return True 275 | 276 | def create_connection(self): 277 | """Create API connection 278 | 279 | 1. Open socket 280 | 2. Log into router 281 | """ 282 | self.open_socket() 283 | self.login() 284 | 285 | def close(self): 286 | self.sock.close() 287 | -------------------------------------------------------------------------------- /rsc2csv.py: -------------------------------------------------------------------------------- 1 | #### Convert the (vougen)rsc File to csv 2 | #### PEZHMAN SHAFIGH 2019 3 | import os 4 | import re 5 | import csv 6 | import datetime 7 | 8 | file_in = 'users.rsc' 9 | file_ou = 'users.csv' 10 | num_lines = sum(1 for line in open(file_in)) 11 | with open(file_in) as file_object: 12 | string = '' 13 | for line in file_object: 14 | string += line.rstrip() +"\n" 15 | 16 | #print(num_lines) 17 | num_id = num_lines-1 18 | num_id = num_lines-1 19 | lni = num_id*5 #last name id 20 | words = (string.split()) 21 | #print(len(words)) 22 | with open(file_ou, 'w') as file_out: 23 | file_out.write("Name,Password,Server,UpTime\n") 24 | start = '=' 25 | end = ',' 26 | for i in range (5,lni,5): 27 | name = ((words[i].split(start))[1].split(end)[0]) 28 | password = ((words[i+1].split(start))[1].split(end)[0]) 29 | server = ((words[i+2].split(start))[1].split(end)[0]) 30 | litime = ((words[i-1].split(start))[1].split(end)[0]) 31 | instr = (name+","+password+","+server+","+litime) 32 | #print(instr) 33 | with open(file_ou, 'a') as file_out: 34 | file_out.write(instr+"\n") 35 | 36 | -------------------------------------------------------------------------------- /samplevoucher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/samplevoucher.png -------------------------------------------------------------------------------- /screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/screenshot01.png -------------------------------------------------------------------------------- /screenshot02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptik/vougen/a391d37c7e73630c9015730cfbf9340384e34637/screenshot02.png -------------------------------------------------------------------------------- /verbose.py: -------------------------------------------------------------------------------- 1 | # Author: Arturs Laizans 2 | # Package for displaying and saving verbose log 3 | 4 | import logging 5 | 6 | 7 | class Log: 8 | 9 | # For initialization, class Log takes 3 arguments: path, logic, file_mode. 10 | # path: 11 | # - False - don't do logging. It won't save anything to file and won't print anything to stdout. 12 | # - True - will print verbose output to stdout. 13 | # - string - will save the verbose output to file named as this string. 14 | # logic: 15 | # - 'OR' - if the path is a string, only saves verbose to file; 16 | # - 'AND' - if the path is string, prints verbose output to stdout and saves to file. 17 | # file_mode: 18 | # - 'a' - appends log to existing file 19 | # - 'w' - creates a new file for logging, if a file with such name already exists, it will be overwritten. 20 | 21 | def __init__(self, path, logic, file_mode): 22 | 23 | # If logging to file is needed, configure it 24 | if path is not True and type(path) == str: 25 | logging.basicConfig(filename=path, filemode=file_mode, 26 | format='%(asctime)s - %(message)s', level=logging.DEBUG) 27 | 28 | # Define different log actions that can be used 29 | def nothing(message): 30 | pass 31 | 32 | def to_file(message): 33 | logging.debug(message) 34 | 35 | def to_stdout(message): 36 | print(message) 37 | 38 | def both(message): 39 | print(message) 40 | logging.debug(message) 41 | 42 | # Set appropriate action depending on path and logic values 43 | if not path: 44 | self.func = nothing 45 | 46 | elif path is True: 47 | self.func = to_stdout 48 | 49 | elif path is not True and type(path) == str and logic == 'OR': 50 | self.func = to_file 51 | 52 | elif path is not True and type(path) == str and logic == 'AND': 53 | self.func = both 54 | else: 55 | self.func = to_stdout 56 | 57 | def __call__(self, message): 58 | self.func(message) 59 | --------------------------------------------------------------------------------