diff --git a/.gitignore b/.gitignore index 166b8f6..8a1d8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,20 @@ linux/debian/ogagent.postinst.debhelper linux/debian/ogagent.postrm.debhelper linux/configure-stamp linux/build-stamp +macos/build +windows/vc_redist.x64.exe ogagent_*_all.deb ogagent_*_amd64.buildinfo ogagent_*_amd64.changes ogagent_*_amd64.build +OGAgentInstaller-*.pkg +OGAgentSetup-*.exe +bin +src/build +src/dist src/about_dialog_ui.py src/message_dialog_ui.py -src/OGAgent_rc.py +src/dist +src/build +windows/VERSION +windows/VC_redist.x64.exe diff --git a/linux/Makefile b/linux/Makefile index 889f07d..95d5c64 100644 --- a/linux/Makefile +++ b/linux/Makefile @@ -39,7 +39,6 @@ install-ogagent: cp $(SOURCEDIR)/OGAgentUser.py $(LIBDIR) # QT Dialogs & resources cp $(SOURCEDIR)/*_ui.py $(LIBDIR) - cp $(SOURCEDIR)/OGAgent_rc.py $(LIBDIR) # Version file cp $(SOURCEDIR)/VERSION $(LIBDIR) diff --git a/linux/debian/changelog b/linux/debian/changelog index e21205c..dc28771 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,11 @@ +ogagent (1.3.1-1) stable; urgency=medium + + * Migrate the update script from shell to python + * pyinstaller: include the 'img' subdir + * take icons from 'img' + + -- OpenGnsys developers Wed, 26 Jun 2024 15:16:57 +0200 + ogagent (1.3.0-2) stable; urgency=medium * Add missing dependency on zenity diff --git a/linux/desktop/OGAgentTool.desktop b/linux/desktop/OGAgentTool.desktop old mode 100755 new mode 100644 diff --git a/macos/build-pkg.sh b/macos/build-pkg.sh index 29f5545..9e112ae 100755 --- a/macos/build-pkg.sh +++ b/macos/build-pkg.sh @@ -1,9 +1,9 @@ #!/bin/bash # Create macOS installation packages. -# Based on bomutils tutorail: http://bomutils.dyndns.org/tutorial.html +# Based on bomutils tutorial: http://bomutils.dyndns.org/tutorial.html cd $(dirname $0) -[ -r ../src/VERSION ] && VERSION="$(cat ../src/VERSION)" || VERSION="1.1.0" +[ -r ../src/VERSION ] && VERSION="$(cat ../src/VERSION)" || (echo "Can't get version from ../src/VERSION" 1>&2; exit 1) AUTHOR="OpenGnsys Project" # Create empty directories. @@ -12,8 +12,8 @@ mkdir -p build && cd build mkdir -p flat/base.pkg flat/Resources/en.lproj mkdir -p root/Applications -# Copy application and script files. -cp -a ../../src root/Applications/OGAgent.app +# Copy application and script files. Exclude 'test_modules' +cp -a ../../src root/Applications/OGAgent.app; rm -rf root/Applications/OGAgent.app/test_modules cp -a ../scripts . # Create plist file. @@ -84,4 +84,3 @@ EOT # Create new Xar application archive. rm -f ../../../OGAgentInstaller-$VERSION.pkg ( cd flat && xar --compression none -cf "../../../OGAgentInstaller-$VERSION.pkg" * ) - diff --git a/macos/scripts/es.opengnsys.ogagent.plist b/macos/scripts/es.opengnsys.agent.system.plist similarity index 76% rename from macos/scripts/es.opengnsys.ogagent.plist rename to macos/scripts/es.opengnsys.agent.system.plist index 5ec11fa..62c68a3 100644 --- a/macos/scripts/es.opengnsys.ogagent.plist +++ b/macos/scripts/es.opengnsys.agent.system.plist @@ -2,10 +2,10 @@ Label - es.opengnsys.ogagent + es.opengnsys.agent.system ProgramArguments - /usr/bin/ogagent + /usr/local/bin/ogagent start RunAtLoad diff --git a/macos/scripts/es.opengnsys.agent.user.plist b/macos/scripts/es.opengnsys.agent.user.plist new file mode 100644 index 0000000..d2f2799 --- /dev/null +++ b/macos/scripts/es.opengnsys.agent.user.plist @@ -0,0 +1,21 @@ + + + + + Label + es.opengnsys.agent.user + EnvironmentVariables + + PATH + /usr/local/bin:/usr/bin:/bin + + WorkingDirectory + /Applications/OGAgent.app + ProgramArguments + + /Applications/OGAgent.app/OGAgentUser.py + + RunAtLoad + + + diff --git a/macos/scripts/ip.py b/macos/scripts/ip.py new file mode 100755 index 0000000..6f73301 --- /dev/null +++ b/macos/scripts/ip.py @@ -0,0 +1,832 @@ +#!/usr/bin/env python3 +# encoding: utf8 + +""" + This program is taken from https://github.com/brona/iproute2mac + When doing 'brew install iproute2mac', we get an old version (1.4.2) that doesn't support 'ip -json' + The alternative installation method recomended in the project's README file is 'curl; chmod; mv' + Therefore we make the decision of shipping this ip.py (version 1.5.0) along opengnsys, which is pretty much the same as curling it +""" + + +""" + iproute2mac + CLI wrapper for basic network utilites on Mac OS X. + Homepage: https://github.com/brona/iproute2mac + + The MIT License (MIT) + Copyright (c) 2015 Bronislav Robenek +""" + +import ipaddress +import json +import os +import random +import re +import socket +import string +import subprocess +import sys +import types + +# Version +VERSION = "1.5.0" + +# Utilities +SUDO = "/usr/bin/sudo" +IFCONFIG = "/sbin/ifconfig" +ROUTE = "/sbin/route" +NETSTAT = "/usr/sbin/netstat" +NDP = "/usr/sbin/ndp" +ARP = "/usr/sbin/arp" +NETWORKSETUP = "/usr/sbin/networksetup" + + +# Helper functions +def perror(*args): + sys.stderr.write(*args) + sys.stderr.write("\n") + + +def execute_cmd(cmd): + print("Executing: %s" % cmd) + status, output = subprocess.getstatusoutput(cmd) + if status == 0: # unix/linux commands 0 true, 1 false + print(output) + return True + else: + perror(output) + return False + + +def json_dump(data, pretty): + if pretty: + print(json.dumps(data, indent=4)) + else: + print(json.dumps(data, separators=(",", ":"))) + return True + +# Classful to CIDR conversion with "default" being passed through +def cidr_from_netstat_dst(target): + if target == "default": + return target + + dots = target.count(".") + if target.find("/") == -1: + addr = target + netmask = (dots + 1) * 8 + else: + [addr, netmask] = target.split("/") + + addr = addr + ".0" * (3 - dots) + return addr + "/" + str(netmask) + + +# Convert hexadecimal netmask in prefix length +def netmask_to_length(mask): + return int(mask, 16).bit_count() + + +def any_startswith(words, test): + for word in words: + if word.startswith(test): + return True + return False + + +# Handles passsing return value, error messages and program exit on error +def help_msg(help_func): + def wrapper(func): + def inner(*args, **kwargs): + if not func(*args, **kwargs): + specific = eval(help_func) + if specific: + if isinstance(specific, types.FunctionType): + if args and kwargs: + specific(*args, **kwargs) + else: + specific() + return False + else: + raise Exception("Function expected for: " + help_func) + else: + raise Exception( + "Function variant not defined: " + help_func + ) + return True + + return inner + + return wrapper + + +# Generate random MAC address with XenSource Inc. OUI +# http://www.linux-kvm.com/sites/default/files/macgen.py +def randomMAC(): + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ":".join(["%02x" % x for x in mac]) + + +# Decode ifconfig output +def parse_ifconfig(res, af, address): + links = [] + count = 1 + + for r in res.split("\n"): + if re.match(r"^\w+:", r): + if count > 1: + links.append(link) + (ifname, flags, mtu, ifindex) = re.findall(r"^(\w+): flags=\d+<(.*)> mtu (\d+) index (\d+)", r)[0] + flags = flags.split(",") + link = { + "ifindex": int(ifindex), + "ifname": ifname, + "flags": flags, + "mtu": int(mtu), + "operstate": "UNKNOWN", + "link_type": "unknown" + } + if "LOOPBACK" in flags: + link["link_type"] = "loopback" + link["address"] = "00:00:00:00:00:00" + link["broadcast"] = "00:00:00:00:00:00" + elif "POINTOPOINT" in flags: + link["link_type"] = "none" + count = count + 1 + else: + if re.match(r"^\s+ether ", r): + link["link_type"] = "ether" + link["address"] = re.findall(r"(\w\w:\w\w:\w\w:\w\w:\w\w:\w\w)", r)[0] + link["broadcast"] = "ff:ff:ff:ff:ff:ff" + elif address and re.match(r"^\s+inet ", r) and af != 6: + (local, netmask) = re.findall(r"inet (\d+\.\d+\.\d+\.\d+) netmask (0x[0-9a-f]+)", r)[0] + addr = { + "family": "inet", + "local": local, + "prefixlen": netmask_to_length(netmask), + } + if re.match(r"^.*broadcast", r): + addr["broadcast"] = re.findall(r"broadcast (\d+\.\d+\.\d+\.\d+)", r)[0] + link["addr_info"] = link.get("addr_info", []) + [addr] + elif address and re.match(r"^\s+inet6 ", r) and af != 4: + (local, prefixlen) = re.findall(r"inet6 ((?:[a-f0-9:]+:+)+[a-f0-9]+)%*\w* +prefixlen (\d+)", r)[0] + link["addr_info"] = link.get("addr_info", []) + [{ + "family": "inet6", + "local": local, + "prefixlen": int(prefixlen) + }] + elif re.match(r"^\s+status: ", r): + match re.findall(r"status: (\w+)", r)[0]: + case "active": + link["operstate"] = "UP" + case "inactive": + link["operstate"] = "DOWN" + + if count > 1: + links.append(link) + + return links + + +def link_addr_show(argv, af, json_print, pretty_json, address): + if len(argv) > 0 and argv[0] == "dev": + argv.pop(0) + if len(argv) > 0: + param = argv[0] + else: + param = "-a" + + status, res = subprocess.getstatusoutput( + IFCONFIG + " -v " + param + " 2>/dev/null" + ) + if status: # unix status + if res == "": + perror(param + " not found") + else: + perror(res) + return False + + links = parse_ifconfig(res, af, address) + + if json_print: + return json_dump(links, pretty_json) + + for l in links: + print("%d: %s: <%s> mtu %d status %s" % ( + l["ifindex"], l["ifname"], ",".join(l["flags"]), l["mtu"], l["operstate"] + )) + print( + " link/" + l["link_type"] + + ((" " + l["address"]) if "address" in l else "") + + ((" brd " + l["broadcast"]) if "broadcast" in l else "") + ) + for a in l.get("addr_info", []): + print( + " %s %s/%d" % (a["family"], a["local"], a["prefixlen"]) + + ((" brd " + a["broadcast"]) if "broadcast" in a else "") + ) + + return True + + +# Help +def do_help(argv=None, af=None, json_print=None, pretty_json=None): + perror("Usage: ip [ OPTIONS ] OBJECT { COMMAND | help }") + perror("where OBJECT := { link | addr | route | neigh }") + perror(" OPTIONS := { -V[ersion] | -j[son] | -p[retty] |") + perror(" -4 | -6 }") + perror("iproute2mac") + perror("Homepage: https://github.com/brona/iproute2mac") + perror( + "This is CLI wrapper for basic network utilities on Mac OS X" + " inspired with iproute2 on Linux systems." + ) + perror( + "Provided functionality is limited and command output is not" + " fully compatible with iproute2." + ) + perror( + "For advanced usage use netstat, ifconfig, ndp, arp, route " + " and networksetup directly." + ) + exit(255) + + +def do_help_route(): + perror("Usage: ip route list") + perror(" ip route get ADDRESS") + perror(" ip route { add | del | replace } ROUTE") + perror(" ip route flush cache") + perror(" ip route flush table main") + perror("ROUTE := NODE_SPEC [ INFO_SPEC ]") + perror("NODE_SPEC := [ TYPE ] PREFIX") + perror("INFO_SPEC := NH") + perror("TYPE := { blackhole }") + perror("NH := { via ADDRESS | gw ADDRESS | nexthop ADDRESS | dev STRING }") + exit(255) + + +def do_help_addr(): + perror("Usage: ip addr show [ dev STRING ]") + perror(" ip addr { add | del } PREFIX dev STRING") + exit(255) + + +def do_help_link(): + perror("Usage: ip link show [ DEVICE ]") + perror(" ip link set dev DEVICE") + perror(" [ { up | down } ]") + perror(" [ address { LLADDR | factory | random } ]") + perror(" [ mtu MTU ]") + exit(255) + + +def do_help_neigh(): + perror("Usage: ip neighbour show [ [ to ] PREFIX ] [ dev DEV ]") + perror(" ip neighbour flush [ dev DEV ]") + exit(255) + + +# Route Module +@help_msg("do_help_route") +def do_route(argv, af, json_print, pretty_json): + if not argv or ( + any_startswith(["show", "lst", "list"], argv[0]) and len(argv) == 1 + ): + return do_route_list(af, json_print, pretty_json) + elif "get".startswith(argv[0]) and len(argv) == 2: + argv.pop(0) + return do_route_get(argv, af, json_print, pretty_json) + elif "add".startswith(argv[0]) and len(argv) >= 3: + argv.pop(0) + return do_route_add(argv, af) + elif "delete".startswith(argv[0]) and len(argv) >= 2: + argv.pop(0) + return do_route_del(argv, af) + elif "replace".startswith(argv[0]) and len(argv) >= 3: + argv.pop(0) + return do_route_del(argv, af) and do_route_add(argv, af) + elif "flush".startswith(argv[0]) and len(argv) >= 1: + argv.pop(0) + return do_route_flush(argv, af) + else: + return False + return True + + +def do_route_list(af, json_print, pretty_json): + # ip route prints IPv6 or IPv4, never both + inet = "inet6" if af == 6 else "inet" + status, res = subprocess.getstatusoutput( + NETSTAT + " -nr -f " + inet + " 2>/dev/null" + ) + if status: + perror(res) + return False + res = res.split("\n") + res = res[4:] # Removes first 4 lines + + routes = [] + + for r in res: + ra = r.split() + target = ra[0] + gw = ra[1] + flags = ra[2] + # macOS Mojave and earlier vs Catalina + dev = ra[5] if len(ra) >= 6 else ra[3] + if flags.find("W") != -1: + continue + if af == 6: + target = re.sub(r"%[^ ]+/", "/", target) + else: + target = cidr_from_netstat_dst(target) + if flags.find("B") != -1: + routes.append({"type": "blackhole", "dst": target, "flags": []}) + continue + if re.match(r"link.+", gw): + routes.append({"dst": target, "dev": dev, "scope": "link", "flags": []}) + else: + routes.append({"dst": target, "gateway": gw, "dev": dev, "flags": []}) + + if json_print: + return json_dump(routes, pretty_json) + + for route in routes: + if "type" in route: + print("%s %s" % (route["type"], route["dst"])) + elif "scope" in route: + print("%s dev %s scope %s" % (route["dst"], route["dev"], route["scope"])) + elif "gateway" in route: + print("%s via %s dev %s" % (route["dst"], route["gateway"], route["dev"])) + + return True + + +def do_route_add(argv, af): + options = "" + if argv[0] == "blackhole": + argv.pop(0) + if len(argv) != 1: + return False + argv.append("via") + argv.append("::1" if ":" in argv[0] or af == 6 else "127.0.0.1") + options = "-blackhole" + + if len(argv) not in (3, 5): + return False + + if len(argv) == 5: + perror( + "iproute2mac: Ignoring last 2 arguments, not implemented: {} {}".format( + argv[3], argv[4] + ) + ) + + if argv[1] in ["via", "nexthop", "gw"]: + gw = argv[2] + elif argv[1] in ["dev"]: + gw = "-interface " + argv[2] + else: + do_help_route() + + prefix = argv[0] + inet = "-inet6 " if ":" in prefix or af == 6 else "" + + return execute_cmd( + SUDO + " " + ROUTE + " add " + inet + prefix + " " + gw + " " + options + ) + + +def do_route_del(argv, af): + options = "" + if argv[0] == "blackhole": + argv.pop(0) + if len(argv) != 1: + return False + if ":" in argv[0] or af == 6: + options = " ::1 -blackhole" + else: + options = " 127.0.0.1 -blackhole" + + prefix = argv[0] + inet = "-inet6 " if ":" in prefix or af == 6 else "" + return execute_cmd( + SUDO + " " + ROUTE + " delete " + inet + prefix + options + ) + + +def do_route_flush(argv, af): + if not argv: + perror('"ip route flush" requires arguments.') + perror("") + return False + + # https://github.com/brona/iproute2mac/issues/38 + # http://linux-ip.net/html/tools-ip-route.html + if argv[0] == "cache": + print("iproute2mac: There is no route cache to flush in MacOS,") + print(" returning 0 status code for compatibility.") + return True + elif len(argv) == 2 and argv[0] == "table" and argv[1] == "main": + family = "-inet6" if af == 6 else "-inet" + print("iproute2mac: Flushing all routes") + return execute_cmd(SUDO + " " + ROUTE + " -n flush " + family) + else: + return False + + +def do_route_get(argv, af, json_print, pretty_json): + target = argv[0] + + inet = "" + if ":" in target or af == 6: + inet = "-inet6 " + family = socket.AF_INET6 + else: + family = socket.AF_INET + + status, res = subprocess.getstatusoutput( + ROUTE + " -n get " + inet + target + ) + if status: # unix status or not in table + perror(res) + return False + if res.find("not in table") >= 0: + perror(res) + exit(1) + + res = dict( + re.findall( + r"^\W*((?:route to|destination|gateway|interface)): (.+)$", + res, + re.MULTILINE, + ) + ) + + route = {"dst": res["route to"], "dev": res["interface"]} + + if "gateway" in res: + route["gateway"] = res["gateway"] + + try: + s = socket.socket(family, socket.SOCK_DGRAM) + s.connect((route["dst"], 7)) + route["prefsrc"] = src_ip = s.getsockname()[0] + s.close() + except: + pass + + route["flags"] = [] + route["uid"] = os.getuid() + route["cache"] = [] + + if json_print: + return json_dump([route], pretty_json) + + print( + route["dst"] + + ((" via " + route["gateway"]) if "gateway" in route else "") + + " dev " + route["dev"] + + ((" src " + route["prefsrc"]) if "prefsrc" in route else "") + + " uid " + str(route["uid"]) + ) + + return True + + +# Addr Module +@help_msg("do_help_addr") +def do_addr(argv, af, json_print, pretty_json): + if not argv: + argv.append("show") + + if any_startswith(["show", "lst", "list"], argv[0]): + argv.pop(0) + return do_addr_show(argv, af, json_print, pretty_json) + elif "add".startswith(argv[0]) and len(argv) >= 3: + argv.pop(0) + return do_addr_add(argv, af) + elif "delete".startswith(argv[0]) and len(argv) >= 3: + argv.pop(0) + return do_addr_del(argv, af) + else: + return False + return True + + +def do_addr_show(argv, af, json_print, pretty_json): + return link_addr_show(argv, af, json_print, pretty_json, True) + + +def do_addr_add(argv, af): + if len(argv) < 2: + return False + + dst = "" + if argv[1] == "peer": + argv.pop(1) + dst = argv.pop(1) + + if argv[1] == "dev": + argv.pop(1) + else: + return False + try: + addr = argv[0] + dev = argv[1] + except IndexError: + perror("dev not found") + exit(1) + inet = "" + if ":" in addr or af == 6: + af = 6 + inet = " inet6" + return execute_cmd( + SUDO + " " + IFCONFIG + " " + dev + inet + " add " + addr + " " + dst + ) + + +def do_addr_del(argv, af): + if len(argv) < 2: + return False + if argv[1] == "dev": + argv.pop(1) + try: + addr = argv[0] + dev = argv[1] + except IndexError: + perror("dev not found") + exit(1) + inet = "inet" + if ":" in addr or af == 6: + af = 6 + inet = "inet6" + return execute_cmd( + SUDO + " " + IFCONFIG + " " + dev + " " + inet + " " + addr + " remove" + ) + + +# Link module +@help_msg("do_help_link") +def do_link(argv, af, json_print, pretty_json): + if not argv: + argv.append("show") + + if any_startswith(["show", "lst", "list"], argv[0]): + argv.pop(0) + return do_link_show(argv, af, json_print, pretty_json) + elif "set".startswith(argv[0]): + argv.pop(0) + return do_link_set(argv, af) + else: + return False + return True + + +def do_link_show(argv, af, json_print, pretty_json): + return link_addr_show(argv, af, json_print, pretty_json, False) + + +def do_link_set(argv, af): + if not argv: + return False + elif argv[0] == "dev": + argv.pop(0) + + if len(argv) < 2: + return False + + dev = argv[0] + + IFCONFIG_DEV_CMD = SUDO + " " + IFCONFIG + " " + dev + try: + args = iter(argv) + for arg in args: + if arg == "up": + if not execute_cmd(IFCONFIG_DEV_CMD + " up"): + return False + elif arg == "down": + if not execute_cmd(IFCONFIG_DEV_CMD + " down"): + return False + elif arg in ["address", "addr", "lladdr"]: + addr = next(args) + if addr in ["random", "rand"]: + addr = randomMAC() + elif addr == "factory": + (status, res) = subprocess.getstatusoutput( + NETWORKSETUP + " -listallhardwareports" + ) + if status != 0: + return False + details = re.findall( + r"^(?:Device|Ethernet Address): (.+)$", + res, + re.MULTILINE, + ) + addr = details[details.index(dev) + 1] + if not execute_cmd(IFCONFIG_DEV_CMD + " lladdr " + addr): + return False + elif arg == "mtu": + mtu = int(next(args)) + if not execute_cmd(IFCONFIG_DEV_CMD + " mtu " + str(mtu)): + return False + except Exception: + return False + return True + + +# Neigh module +@help_msg("do_help_neigh") +def do_neigh(argv, af, json_print, pretty_json): + if not argv: + argv.append("show") + + if any_startswith(["show", "list", "lst"], argv[0]) and len(argv) <= 5: + argv.pop(0) + return do_neigh_show(argv, af, json_print, pretty_json) + elif "flush".startswith(argv[0]): + argv.pop(0) + return do_neigh_flush(argv, af) + else: + return False + + +def do_neigh_show(argv, af, json_print, pretty_json): + prefix = None + dev = None + try: + while argv: + arg = argv.pop(0) + if arg == "to": + prefix = argv.pop(0) + elif arg == "dev": + dev = argv.pop(0) + elif prefix is None: + prefix = arg + else: + return False + if prefix: + prefix = ipaddress.ip_network(prefix, strict=False) + except Exception: + return False + + nd_ll_states = { + "R": "REACHABLE", + "S": "STALE", + "D": "DELAY", + "P": "PROBE", + "I": "INCOMPLETE", + "N": "INCOMPLETE", + "W": "INCOMPLETE", + } + + neighs = [] + + if af != 4: + res = subprocess.run( + [NDP, "-an"], capture_output=True, text=True, check=True + ) + for row in res.stdout.splitlines()[1:]: + cols = row.split() + entry = {"dst": re.sub(r"%.+$", "", cols[0])} + if cols[1] != "(incomplete)": + entry["lladdr"] = cols[1] + entry["dev"] = cols[2] + if dev and entry["dev"] != dev: + continue + if prefix and ipaddress.ip_address(entry["dst"]) not in prefix: + continue + if cols[1] == "(incomplete)" and cols[4] != "R": + entry["status"] = ["INCOMPLETE"] + else: + entry["status"] = [nd_ll_states[cols[4]]] + entry["router"] = len(cols) >= 6 and cols[5] == "R" + neighs.append(entry) + + if af != 6: + args = [ARP, "-anl"] + if dev: + args += ["-i", dev] + + res = subprocess.run(args, capture_output=True, text=True, check=True) + for row in res.stdout.splitlines()[1:]: + cols = row.split() + entry = {"dst": cols[0]} + if cols[1] != "(incomplete)": + entry["lladdr"] = cols[1] + entry["dev"] = cols[4] + if dev and entry["dev"] != dev: + continue + if prefix and ipaddress.ip_address(entry["dst"]) not in prefix: + continue + if cols[1] == "(incomplete)": + entry["status"] = ["INCOMPLETE"] + else: + entry["status"] = ["REACHABLE"] + entry["router"] = False + neighs.append(entry) + + if json_print: + return json_dump(neighs, pretty_json) + + for nb in neighs: + print( + nb["dst"] + + ("", " dev " + nb["dev"], "")[dev == None] + + ("", " router")[nb["router"]] + + " %s" % (nb["status"][0]) + ) + + return True + + +def do_neigh_flush(argv, af): + if len(argv) != 2: + perror("Flush requires arguments.") + exit(1) + + if argv[0] != "dev": + return False + dev = argv[1] + + if af != 4: + print( + "iproute2mac: NDP doesn't support filtering by interface," + "flushing all IPv6 entries." + ) + execute_cmd(SUDO + " " + NDP + " -cn") + if af != 6: + execute_cmd(SUDO + " " + ARP + " -a -d -i " + dev) + return True + + +# Match iproute2 commands +# https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip.c#n86 +cmds = [ + ("address", do_addr), + ("route", do_route), + ("neighbor", do_neigh), + ("neighbour", do_neigh), + ("link", do_link), + ("help", do_help), +] + + +@help_msg("do_help") +def main(argv): + af = -1 # default / both + json_print = False + pretty_json = False + + while argv and argv[0].startswith("-"): + # Turn --opt into -opt + argv[0] = argv[0][1:] if argv[0][1] == "-" else argv[0] + # Process options + if argv[0] == "-6": + af = 6 + argv.pop(0) + elif argv[0] == "-4": + af = 4 + argv.pop(0) + elif argv[0].startswith("-color"): + perror("iproute2mac: Color option is not implemented") + argv.pop(0) + elif "-json".startswith(argv[0]): + json_print = True + argv.pop(0) + elif "-pretty".startswith(argv[0]): + pretty_json = True + argv.pop(0) + elif "-Version".startswith(argv[0]): + print("iproute2mac, v" + VERSION) + exit(0) + elif "-help".startswith(argv[0]): + return False + else: + perror('Option "{}" is unknown, try "ip help".'.format(argv[0])) + exit(255) + + if not argv: + return False + + for cmd, cmd_func in cmds: + if cmd.startswith(argv[0]): + argv.pop(0) + # Functions return true or terminate with exit(255) + # See help_msg and do_help* + return cmd_func(argv, af, json_print, pretty_json) + + perror('Object "{}" is unknown, try "ip help".'.format(argv[0])) + exit(1) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/macos/scripts/ogagent b/macos/scripts/ogagent index b139d7f..0839607 100755 --- a/macos/scripts/ogagent +++ b/macos/scripts/ogagent @@ -3,4 +3,4 @@ FOLDER=/Applications/OGAgent.app cd $FOLDER -python -m opengnsys.linux.OGAgentService $@ +/usr/local/bin/python3 -m opengnsys.linux.OGAgentService $@ diff --git a/macos/scripts/postinstall b/macos/scripts/postinstall index ebb2ca4..dc22183 100755 --- a/macos/scripts/postinstall +++ b/macos/scripts/postinstall @@ -1,17 +1,11 @@ #!/usr/bin/env sh -# Directories SRCDIR=$(dirname "$0") -BINDIR=/usr/bin -INITDIR=/Library/LaunchDaemons +BINDIR=/usr/local/bin +LAUNCH_AGENTS_DIR=/Library/LaunchAgents +LAUNCH_DAEMONS_DIR=/Library/LaunchDaemons -# Check if it needs to install Python dependencies: -if ! which pip &>/dev/null; then - easy_install pip - pip install netifaces requests six -fi - -# Copying files. cp $SRCDIR/ogagent $BINDIR -cp $SRCDIR/es.opengnsys.ogagent.plist $INITDIR - +cp $SRCDIR/ip.py $BINDIR/ip ## override 'ip' from iproute2mac-1.4.2 with a more recent one +cp $SRCDIR/es.opengnsys.agent.system.plist $LAUNCH_DAEMONS_DIR +cp $SRCDIR/es.opengnsys.agent.user.plist $LAUNCH_AGENTS_DIR diff --git a/src/OGAgent.spec b/src/OGAgent.spec new file mode 100755 index 0000000..36eebcd --- /dev/null +++ b/src/OGAgent.spec @@ -0,0 +1,97 @@ +# -*- mode: python ; coding: utf-8 -*- + + +ogausr_a = Analysis( + ['OGAgentUser.py'], + pathex=[], + binaries=[], + datas=[ +# ('cfg', 'cfg'), ## add the entire directory + ('img', 'img'), ## add the entire directory + ], + hiddenimports=['win32timezone', 'socketserver', 'http.server', 'urllib'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +ogasvc_a = Analysis( + ['opengnsys\\windows\\OGAgentService.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=['win32timezone', 'socketserver', 'http.server', 'urllib'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) + +MERGE( + (ogausr_a, 'OGAgentUser', 'OGAgentUser'), ## class, py name, exe name + (ogasvc_a, 'OGAgentService', 'OGAgentService') +) + +ogausr_pyz = PYZ(ogausr_a.pure) +ogasvc_pyz = PYZ(ogasvc_a.pure) + +ogausr_exe = EXE( + ogausr_pyz, + ogausr_a.scripts, + [], + exclude_binaries=True, + name='OGAgentUser', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['img\\oga.ico'], +) +ogasvc_exe = EXE( + ogasvc_pyz, + ogasvc_a.scripts, + [], + exclude_binaries=True, + name='OGAgentService', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['img\\oga.ico'], + manifest='OGAgent.manifest', +) + +dist_name = 'OGAgent' +coll = COLLECT( + ogausr_exe, + ogausr_a.binaries, + ogausr_a.datas, + + ogasvc_exe, + ogasvc_a.binaries, + ogasvc_a.datas, + + strip=False, + upx=True, + upx_exclude=[], + name=dist_name, +) + +import shutil +shutil.copytree ('cfg', '{}/{}/cfg'.format(DISTPATH, dist_name)) diff --git a/src/OGAgentUser.py b/src/OGAgentUser.py old mode 100644 new mode 100755 index d4ec146..9a9eab0 --- a/src/OGAgentUser.py +++ b/src/OGAgentUser.py @@ -34,6 +34,7 @@ import base64 import json import sys import time +import os from PyQt6 import QtCore, QtGui, QtWidgets from about_dialog_ui import Ui_OGAAboutDialog @@ -159,7 +160,8 @@ class OGASystemTray(QtWidgets.QSystemTrayIcon): self.ipcport = int(cfg.get('ipc_port', IPC_PORT)) - icon = QtGui.QIcon(':/images/img/oga.png') + QtCore.QDir.addSearchPath('images', os.path.join(os.path.dirname(__file__), 'img')) + icon = QtGui.QIcon('images:oga.png') QtWidgets.QSystemTrayIcon.__init__(self, icon, parent) self.menu = QtWidgets.QMenu(parent) diff --git a/src/VERSION b/src/VERSION index f0bb29e..31e5c84 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.3.0 +1.3.3 diff --git a/src/opengnsys/config.py b/src/opengnsys/config.py index 7ffe046..4228b39 100644 --- a/src/opengnsys/config.py +++ b/src/opengnsys/config.py @@ -32,7 +32,7 @@ # pylint: disable=unused-wildcard-import, wildcard-import -from configparser import SafeConfigParser +from configparser import ConfigParser config = None @@ -45,7 +45,7 @@ def readConfig(client=False): This is this way so we can protect ogagent.cfg against reading for non admin users on all platforms. """ - cfg = SafeConfigParser() + cfg = ConfigParser() if client is True: fname = 'ogclient.cfg' else: diff --git a/src/opengnsys/httpserver.py b/src/opengnsys/httpserver.py index 69c4317..a545b9b 100644 --- a/src/opengnsys/httpserver.py +++ b/src/opengnsys/httpserver.py @@ -57,7 +57,8 @@ class HTTPServerHandler(BaseHTTPRequestHandler): return def sendJsonResponse(self, data): - self.send_response(200) + try: self.send_response(200) + except Exception as e: logger.warn (str(e)) data = json.dumps(data) self.send_header('Content-type', 'application/json') self.send_header('Content-Length', str(len(data))) @@ -117,7 +118,7 @@ class HTTPServerHandler(BaseHTTPRequestHandler): logger.error('HTTP ' + fmt % args) def log_message(self, fmt, *args): - logger.info('HTTP ' + fmt % args) + logger.debug('HTTP ' + fmt % args) class HTTPThreadingServer(ThreadingMixIn, HTTPServer): @@ -132,7 +133,9 @@ class HTTPServerThread(threading.Thread): self.certFile = createSelfSignedCert() self.server = HTTPThreadingServer(address, HTTPServerHandler) - self.server.socket = ssl.wrap_socket(self.server.socket, certfile=self.certFile, server_side=True) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(certfile=self.certFile) + self.server.socket = context.wrap_socket(self.server.socket, server_side=True) logger.debug('Initialized HTTPS Server thread on {}'.format(address)) diff --git a/src/opengnsys/macos/operations.py b/src/opengnsys/macos/operations.py index 15b6b11..2d69aec 100644 --- a/src/opengnsys/macos/operations.py +++ b/src/opengnsys/macos/operations.py @@ -42,45 +42,69 @@ import subprocess import struct import array import six +import json from opengnsys import utils -import netifaces +ip_a_s = None + +## make sure /usr/local/bin is in PATH +## we need that to run /usr/local/bin/ip, which uses 'env' and pulls from PATH anyway +def check_path(): + path = os.getenv ('PATH', '') + usr_local_bin = '/usr/local/bin' + + if usr_local_bin not in path.split (os.pathsep): + os.environ['PATH'] = usr_local_bin + os.pathsep + path def _getMacAddr(ifname): ''' Returns the mac address of an interface Mac is returned as unicode utf-8 encoded ''' - if isinstance(ifname, list): - return dict([(name, _getMacAddr(name)) for name in ifname]) - if isinstance(ifname, six.text_type): - ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) - try: - return netifaces.ifaddresses(ifname)[18][0]['addr'] - except Exception: - return None + for interface in ip_a_s: + if interface.get ('ifname') != ifname: continue + return interface.get ('address') + return None def _getIpAddr(ifname): ''' - Returns the IP address of an interface + Returns the first IP address of an interface + IPv4 is preferred over IPv6 IP is returned as unicode utf-8 encoded ''' - if isinstance(ifname, list): - return dict([(name, _getIpAddr(name)) for name in ifname]) - if isinstance(ifname, six.text_type): - ifname = ifname.encode('utf-8') # If unicode, convert to bytes (or str in python 2.7) - try: - return netifaces.ifaddresses(ifname)[2][0]['addr'] - except Exception: - return None + + ## loop and return the first IPv4 address found + for interface in ip_a_s: + if interface.get ('ifname') != ifname: continue + for addr_info in interface.get ('addr_info', []): + ip_address = addr_info.get ('local') + try: + ip_address.index ('.') + return ip_address + except: pass + + ## if nothing found, loop again and return the first IP found, which will be an IPv6 + for interface in ip_a_s: + if interface.get ('ifname') != ifname: continue + for addr_info in interface.get ('addr_info', []): + return addr_info.get ('local') + + return None def _getInterfaces(): ''' Returns a list of interfaces names ''' - return netifaces.interfaces() + global ip_a_s + + check_path() + result = subprocess.run (['/usr/local/bin/ip', '-json', 'address', 'show'], capture_output=True, text=True) + + if result.returncode != 0: raise Exception (f'Command "ip" failed with exit code {result.returncode}') + ip_a_s = json.loads (result.stdout) + return [i.get('ifname') for i in ip_a_s] def _getIpAndMac(ifname): @@ -152,7 +176,7 @@ def logoff(): import threading threading._DummyThread._Thread__stop = lambda x: 42 - # Exec logout using AppleSctipt + # Exec logout using AppleScript subprocess.call('/usr/bin/osascript -e \'tell app "System Events" to «event aevtrlgo»\'', shell=True) @@ -244,7 +268,19 @@ def getSessionLanguage(): ''' Returns the user's session language ''' - return locale.getdefaultlocale()[0] + lang = locale.getdefaultlocale()[0] + if lang is None: + return 'C' + else: + return lang + + +def get_session_type(): + """ + Minimal implementation of this required function + :return: string + """ + return 'unknown' def showPopup(title, message): diff --git a/src/opengnsys/modules/server/OpenGnSys/__init__.py b/src/opengnsys/modules/server/OpenGnSys/__init__.py index 73182b6..d7854d4 100644 --- a/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -65,7 +65,7 @@ def check_secret(fnc): else: raise Exception('Unauthorized operation') except Exception as e: - logger.error(e) + logger.error(str(e)) raise Exception(e) return wrapper @@ -83,7 +83,7 @@ def execution_level(level): else: raise Exception('Unauthorized operation') except Exception as e: - logger.error(e) + logger.error(str(e)) raise Exception(e) return wrapper @@ -149,6 +149,7 @@ class OpenGnSysWorker(ServerWorker): break # Raise error after timeout if not self.interface: + ## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value raise e # Loop to send initialization message @@ -332,11 +333,23 @@ class OpenGnSysWorker(ServerWorker): :return: JSON object {"op": "launched"} """ logger.debug('Processing script request') - # Decoding script (Windows scripts need a subprocess call per line) + # Decoding script script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8')) + logger.debug('received script {}'.format(script)) if operations.os_type == 'Windows': - script = 'import subprocess; {0}'.format( - ';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')])) + ## for windows, we turn the script into utf16le, then to b64 again, and feed the blob to powershell + u16 = script.encode ('utf-16le') ## utf16 + b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8)) + script = """ +import os +import tempfile +import subprocess +cp = subprocess.run ("powershell -WindowStyle Hidden -EncodedCommand {}", capture_output=True) +subprocs_log = os.path.join (tempfile.gettempdir(), 'opengnsys-subprocs.log') +with open (subprocs_log, 'ab') as fd: ## TODO improve this logging + fd.write (cp.stdout) + fd.write (cp.stderr) +""".format (b64) else: script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script) # Executing script. diff --git a/src/opengnsys/utils.py b/src/opengnsys/utils.py index 36bef63..c54b4b7 100644 --- a/src/opengnsys/utils.py +++ b/src/opengnsys/utils.py @@ -61,7 +61,7 @@ def exceptionToMessage(e): if isinstance(arg, Exception): msg = msg + exceptionToMessage(arg) else: - msg = msg + toUnicode(arg) + '. ' + msg = msg + str(arg) + '. ' return msg diff --git a/src/opengnsys/windows/OGAgentService.py b/src/opengnsys/windows/OGAgentService.py index 71716d7..d42c144 100644 --- a/src/opengnsys/windows/OGAgentService.py +++ b/src/opengnsys/windows/OGAgentService.py @@ -120,5 +120,9 @@ class OGAgentSvc(win32serviceutil.ServiceFramework, CommonService): if __name__ == '__main__': - - win32serviceutil.HandleCommandLine(OGAgentSvc) + if len(sys.argv) == 1: + servicemanager.Initialize() + servicemanager.PrepareToHostSingle(OGAgentSvc) + servicemanager.StartServiceCtrlDispatcher() + else: + win32serviceutil.HandleCommandLine(OGAgentSvc) diff --git a/src/opengnsys/windows/log.py b/src/opengnsys/windows/log.py index 745fd03..7b2bec2 100644 --- a/src/opengnsys/windows/log.py +++ b/src/opengnsys/windows/log.py @@ -43,11 +43,11 @@ OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6)) class LocalLogger(object): def __init__(self): # tempdir is different for "user application" and "service" - # service wil get c:\windows\temp, while user will get c:\users\XXX\temp + # service wil get c:\windows\temp, while user will get c:\users\XXX\appdata\local\temp logging.basicConfig( filename=os.path.join(tempfile.gettempdir(), 'opengnsys.log'), filemode='a', - format='%(levelname)s %(asctime)s %(message)s', + format='%(levelname)s %(asctime)s (%(threadName)s) %(message)s', level=logging.DEBUG ) self.logger = logging.getLogger('opengnsys') @@ -58,7 +58,7 @@ class LocalLogger(object): # our loglevels are 10000 (other), 20000 (debug), .... # logging levels are 10 (debug), 20 (info) # OTHER = logging.NOTSET - self.logger.log(level / 1000 - 10, message) + self.logger.log(int(level / 1000 - 10), message) if level < INFO or self.serviceLogger is False: # Only information and above will be on event log return diff --git a/src/opengnsys/windows/operations.py b/src/opengnsys/windows/operations.py index 82b6522..c6f0e11 100644 --- a/src/opengnsys/windows/operations.py +++ b/src/opengnsys/windows/operations.py @@ -64,7 +64,7 @@ def getNetworkInfo(): ip: ip of the interface ''' obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") - wmobj = obj.ConnectServer("localhost", "root\cimv2") + wmobj = obj.ConnectServer("localhost", "root\\cimv2") adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True") try: for obj in adapters: @@ -102,12 +102,12 @@ def getWindowsVersion(): ''' Returns Windows version. ''' - import _winreg - reg = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion') + import winreg + reg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion') try: - data = '{} {}'.format(_winreg.QueryValueEx(reg, 'ProductName')[0], _winreg.QueryValueEx(reg, 'ReleaseId')[0]) + data = '{} {}'.format(winreg.QueryValueEx(reg, 'ProductName')[0], winreg.QueryValueEx(reg, 'ReleaseId')[0]) except Exception: - data = '{} {}'.format(_winreg.QueryValueEx(reg, 'ProductName')[0], _winreg.QueryValueEx(reg, 'CurrentBuildNumber')[0]) + data = '{} {}'.format(winreg.QueryValueEx(reg, 'ProductName')[0], winreg.QueryValueEx(reg, 'CurrentBuildNumber')[0]) reg.Close() return data diff --git a/src/requirements.txt b/src/requirements.txt index abedc7c..b85c90d 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,3 +1,6 @@ -netifaces +pywin32 +pyqt6 requests -urllib3 +six +pycryptodome +pyinstaller diff --git a/src/setup.py b/src/setup.py deleted file mode 100644 index a3b7f43..0000000 --- a/src/setup.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2014 Virtual Cable S.L. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -@author: Adolfo Gómez, dkmaster at dkmon dot com -@author: Ramón M. Gómez, ramongomez at us dot es -""" - -# ModuleFinder can't handle runtime changes to __path__, but win32com uses them -try: - # py2exe 0.6.4 introduced a replacement modulefinder. - # This means we have to add package paths there, not to the built-in - # one. If this new modulefinder gets integrated into Python, then - # we might be able to revert this some day. - # if this doesn't work, try import modulefinder - try: - import py2exe.mf as modulefinder - except ImportError: - import modulefinder - import win32com - import sys - for p in win32com.__path__[1:]: - modulefinder.AddPackagePath("win32com", p) - for extra in ["win32com.shell"]: # ,"win32com.mapi" - __import__(extra) - m = sys.modules[extra] - for p in m.__path__[1:]: - modulefinder.AddPackagePath(extra, p) -except ImportError: - # no build path setup, no worries. - pass - -import os -from distutils.core import setup -import py2exe -import sys - -# update.sh changes this value -VERSION='1.3.0' - -sys.argv.append('py2exe') - - -def get_requests_cert_file(): - """Add Python requests or certifi .pem file for installers.""" - import requests - f = os.path.join(os.path.dirname(requests.__file__), 'cacert.pem') - if not os.path.exists(f): - import certifi - f = os.path.join(os.path.dirname(certifi.__file__), 'cacert.pem') - return f - - -class Target: - - def __init__(self, **kw): - self.__dict__.update(kw) - # for the versioninfo resources - self.version = VERSION - self.name = 'OGAgentService' - self.description = 'OpenGnsys Agent Service' - self.author = 'Adolfo Gomez' - self.url = 'https://opengnsys.es/' - self.company_name = "OpenGnsys Project" - self.copyright = "(c) 2014 VirtualCable S.L.U." - self.name = "OpenGnsys Agent" - -# Now you need to pass arguments to setup -# windows is a list of scripts that have their own UI and -# thus don't need to run in a console. - - -ogaservice = Target( - description='OpenGnsys Agent Service', - modules=['opengnsys.windows.OGAgentService'], - icon_resources=[(0, 'img\\oga.ico'), (1, 'img\\oga.ico')], - cmdline_style='pywin32' -) - -# Some test_modules are hidden to py2exe by six, we ensure that they appear on "includes" -HIDDEN_BY_SIX = ['SocketServer', 'SimpleHTTPServer', 'urllib'] - -setup( - windows=[ - { - 'script': 'OGAgentUser-qt4.py', - 'icon_resources': [(0, 'img\\oga.ico'), (1, 'img\\oga.ico')] - }, - ], - console=[ - { - 'script': 'OGAServiceHelper.py' - } - ], - service=[ogaservice], - data_files=[('', [get_requests_cert_file()]), ('cfg', ['cfg/ogagent.cfg', 'cfg/ogclient.cfg'])], - options={ - 'py2exe': { - 'bundle_files': 3, - 'compressed': True, - 'optimize': 2, - 'includes': ['sip', 'PyQt4', 'win32com.shell', 'requests', 'encodings', 'encodings.utf_8'] + HIDDEN_BY_SIX, - 'excludes': ['doctest', 'unittest'], - 'dll_excludes': ['msvcp90.dll'], - 'dist_dir': '..\\bin', - } - }, - name='OpenGnsys Agent', - version=VERSION, - description='OpenGnsys Agent', - author='Adolfo Gomez', - author_email='agomez@virtualcable.es', - zipfile='OGAgent.zip', -) diff --git a/src/update.py b/src/update.py new file mode 100755 index 0000000..11f2648 --- /dev/null +++ b/src/update.py @@ -0,0 +1,40 @@ +#!/usr/bin/python3 + +import os +import re +import sys +import subprocess +import fileinput + +def update_version(): + if os.path.isfile ('VERSION'): + with open ('VERSION', 'r') as version_file: + version = version_file.read().strip() + + pattern = r'[0-9]*\.[0-9]*\.[0-9]*' + matches = re.findall (pattern, version) + win_version = matches[0] + + with fileinput.FileInput ('about-dialog.ui', inplace=True) as file: + for line in file: + print (line.replace ('Version [^<]*', f'Version {version}'), end='') + + with fileinput.FileInput ('opengnsys/__init__.py', inplace=True) as file: + for line in file: + print(line.replace ('VERSION=.*', f"VERSION='{version}'"), end='') + + with open ('../windows/VERSION', 'w') as outfile: + outfile.write (win_version + '\n') + + else: + print ('VERSION: No such file or directory') + sys.exit (1) + +def process_ui(): + subprocess.run (['pyuic6', 'about-dialog.ui', '-o', 'about_dialog_ui.py', '-x']) + subprocess.run (['pyuic6', 'message-dialog.ui', '-o', 'message_dialog_ui.py', '-x']) + +if __name__ == "__main__": + os.chdir (os.path.dirname (os.path.abspath (__file__))) + update_version() + process_ui() diff --git a/src/update.sh b/src/update.sh deleted file mode 100755 index 3f39fed..0000000 --- a/src/update.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# -# Copyright (c) 2014 Virtual Cable S.L. -# Copyright (c) 2024 Qindel Formación y Servicios S.L. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -function update_version { - if [[ -r VERSION ]]; then - V="$(cat VERSION)" - sed -i "s/Version [^<]*/Version $V/" about-dialog.ui - sed -i "s/^VERSION='.*'$/VERSION='$V'/" setup.py - sed -i "s/^VERSION='.*'$/VERSION='$V'/" opengnsys/__init__.py - else - echo 'src/VERSION: No such file or directory' - exit 1 - fi -} - -function process_ui { - pyuic6 about-dialog.ui -o about_dialog_ui.py -x - pyuic6 message-dialog.ui -o message_dialog_ui.py -x -} - -function process_resources { - ## requires a virtualenv with pyside6 - ## you can create it by doing 'mkvirtualenv -p python3 ogpyside6' - ## this will obviously go away in the future, but we need to merge the py3/qt6 change now - ~/.virtualenvs/ogpyside6/bin/pyside6-rcc -o OGAgent_rc.py OGAgent.qrc - sed -i -e '/^from PySide6 import QtCore/s/PySide6/PyQt6/' OGAgent_rc.py -} - -cd $(dirname "$0") -update_version -process_ui -process_resources diff --git a/windows/build-windows.sh b/windows/build-windows.sh deleted file mode 100755 index 88a3c4e..0000000 --- a/windows/build-windows.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" -export WINEARCH=win32 -export WINEPREFIX=$PWD/wine -grep -o "[0-9]*\.[0-9]*\.[0-9]*" ../src/VERSION > VERSION -wine cmd /c c:\\ogagent\\build.bat -chmod -x ../OGAgentSetup*.exe diff --git a/windows/build.bat b/windows/build.bat deleted file mode 100644 index 0e0d514..0000000 --- a/windows/build.bat +++ /dev/null @@ -1,7 +0,0 @@ -C: -CD \ogagent\src -python setup.py -CD .. -RENAME bin\OGAgentUser-qt4.exe OGAgentUser.exe -"C:\Program Files\NSIS\makensis.exe" ogagent.nsi - diff --git a/windows/ogagent.nsi b/windows/ogagent.nsi index a08dbbc..7aaf027 100644 --- a/windows/ogagent.nsi +++ b/windows/ogagent.nsi @@ -88,8 +88,8 @@ Section -Main SEC0000 SetShellVarContext all SetOutPath $INSTDIR SetOverwrite on - File /r bin\*.* - File vcredist_x86.exe + File /r src\dist\OGAgent\*.* + File windows\VC_redist.x64.exe File src\VERSION WriteRegStr HKLM "${REGKEY}\Components" Main 1 SectionEnd @@ -111,16 +111,16 @@ Section -post SEC0001 WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" OGAgentTool $INSTDIR\OGAgentUser.exe WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoModify 1 WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$(^Name)" NoRepair 1 - ExecWait '"$INSTDIR\vcredist_x86.exe" /passive /norestart' + ExecWait '"$INSTDIR\VC_redist.x64.exe" /passive /norestart' # Add the application to the firewall exception list - All Networks - All IP Version - Enabled # SimpleFC::AddApplication "OpenGnsys Agent Service" "$INSTDIR\OGAgentService.exe" 0 2 "" 1 - # SimpleFC::AdvAddRule [name] [description] [protocol] [direction] - # [status] [profile] [action] [application] [service_name] [icmp_types_and_codes] + # SimpleFC::AdvAddRule [name] [description] [protocol] [direction] + # [status] [profile] [action] [application] [service_name] [icmp_types_and_codes] # [group] [local_ports] [remote_ports] [local_address] [remote_address] # SimpleFC::AdvAddRule "OpenGnsys Agent Firewall rules" "Firewall rules for OpenGnsys Agent interaction with broker." "6" "1" \ "1" "7" "1" "$INSTDIR\OGAgentService.exe" "" "" \ - "" "" "" "" "" + "" "" "" "" "" Pop $0 ; return error(1)/success(0) # Disable fast boot on Windows 10, if registry key exists. ReadRegDWORD $0 HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Power" HiberbootEnabled @@ -128,8 +128,6 @@ Section -post SEC0001 WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Power" HiberbootEnabled 0 # Install service nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe --startup auto install" # Add service after installation - # Update recovery options - nsExec::Exec /OEM "$INSTDIR\OGAServiceHelper.exe" Exec "net start ogagent" Exec "$INSTDIR\OGAgentUser.exe" SectionEnd @@ -151,7 +149,7 @@ done${UNSECTION_ID}: Section /o -un.Main UNSEC0000 nsExec::Exec "taskkill /F /IM OGAgentUser.exe /T" nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe stop" # Stops the service prior uninstall - nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe remove" # Removes the service prior uninstall + nsExec::Exec /OEM "$INSTDIR\OGAgentService.exe remove" # Removes the service prior uninstall nsExec::Exec "taskkill /F /IM OGAgentService.exe /T" Delete /REBOOTOK "$INSTDIR\*.*" DeleteRegValue HKLM "${REGKEY}\Components" Main @@ -182,7 +180,7 @@ Function .onInit InitPluginsDir Call GetParameters StrCpy $StartMenuGroup "OpenGnsys Agent" - + !insertmacro MUI_LANGDLL_DISPLAY !insertmacro MULTIUSER_INIT FunctionEnd diff --git a/windows/py2exe-wine-linux.sh b/windows/py2exe-wine-linux.sh deleted file mode 100755 index 68229b0..0000000 --- a/windows/py2exe-wine-linux.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/sh - -# We need: -# * Wine (32 bit) -# * winetricks (in some distributions) - -export WINEARCH=win32 WINEPREFIX=$PWD/wine WINEDEBUG=fixme-all -WINE=wine - -download() { - mkdir downloads - # Get needed software - cd downloads - wget -nd https://www.python.org/ftp/python/2.7.17/python-2.7.17.msi -O python-2.7.msi - wget -nd https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi - wget -nd https://bootstrap.pypa.io/get-pip.py - wget -nd https://sourceforge.net/projects/pyqt/files/PyQt4/PyQt-4.11.4/PyQt4-4.11.4-gpl-Py2.7-Qt4.8.7-x32.exe/download -O pyqt-install.exe - wget -nd https://prdownloads.sourceforge.net/nsis/nsis-3.05-setup.exe?download -O nsis-install.exe - wget -nd http://nsis.sourceforge.net/mediawiki/images/d/d7/NSIS_Simple_Firewall_Plugin_1.20.zip - cd .. -} - -install_python() { - if which winetricks &>/dev/null; then - echo "Setting up wine prefix (using winetricks)" - winetricks - fi - - cd downloads - echo "Installing python" - $WINE msiexec /qn /i python-2.7.msi - echo "Installing vc for python" - $WINE msiexec /qn /i VCForPython27.msi - - echo "Installing pyqt (needs X)" - $WINE pyqt-install.exe - echo "Installing nsis (needs X?)" - $WINE nsis-install.exe - - cd .. -} - -setup_pip() { - echo "Seting up pip..." - $WINE C:\\Python27\\python -m pip install --upgrade pip -} - -install_packages() { - echo "Installing pywin32" - $WINE C:\\Python27\\python -m pip install pywin32 - echo "Installing py2exe" - $WINE C:\\Python27\\python -m pip install py2exe_py2 - echo "Installing required packages" - $WINE C:\\Python27\\python -m pip install requests six - # Using easy_install instead of pip to install pycrypto - $WINE C:\\Python27\\Scripts\\easy_install http://www.voidspace.org.uk/python/pycrypto-2.6.1/pycrypto-2.6.1.win32-py2.7.exe - # Copy nsis required NSIS_Simple_Firewall_Plugin_1 - echo "Copying simple firewall plugin for nsis installer" - unzip -o downloads/NSIS_Simple_Firewall_Plugin_1.20.zip SimpleFC.dll -d $WINEPREFIX/drive_c/Program\ Files/NSIS/Plugins/x86-ansi/ - unzip -o downloads/NSIS_Simple_Firewall_Plugin_1.20.zip SimpleFC.dll -d $WINEPREFIX/drive_c/Program\ Files/NSIS/Plugins/x86-unicode/ -} - -download -install_python -setup_pip -install_packages - diff --git a/windows/setup.bat b/windows/setup.bat new file mode 100644 index 0000000..517a588 --- /dev/null +++ b/windows/setup.bat @@ -0,0 +1,43 @@ +C: +cd \Users\Docker\Downloads +mkdir setup + +rem creamos directorio setup, nos bajamos cosas y mas tarde lo borramos +rem pero el VC_redist hace falta para el empaquetado, asi que lo descargamos fuera de setup para no borrarlo + +curl https://www.python.org/ftp/python/3.12.3/amd64/core.msi --output setup\core.msi +curl https://www.python.org/ftp/python/3.12.3/amd64/dev.msi --output setup\dev.msi +curl https://www.python.org/ftp/python/3.12.3/amd64/exe.msi --output setup\exe.msi +curl https://www.python.org/ftp/python/3.12.3/amd64/lib.msi --output setup\lib.msi +curl https://www.python.org/ftp/python/3.12.3/amd64/path.msi --output setup\path.msi +curl https://www.python.org/ftp/python/3.12.3/amd64/pip.msi --output setup\pip.msi +curl https://aka.ms/vs/17/release/vc_redist.x64.exe --location --output VC_redist.x64.exe +curl https://prdownloads.sourceforge.net/nsis/nsis-3.05-setup.exe?download --location --output setup\nsis-install.exe +curl http://nsis.sourceforge.net/mediawiki/images/d/d7/NSIS_Simple_Firewall_Plugin_1.20.zip --location --output setup\NSIS_Simple_Firewall_Plugin_1.20.zip + +cd setup +msiexec /i core.msi TARGETDIR=C:\Python312 +msiexec /i dev.msi TARGETDIR=C:\Python312 +msiexec /i exe.msi TARGETDIR=C:\Python312 +msiexec /i lib.msi TARGETDIR=C:\Python312 +msiexec /i path.msi TARGETDIR=C:\Python312 +msiexec /i pip.msi TARGETDIR=C:\Python312 +cd .. +VC_redist.x64.exe /install /quiet /passive /norestart + +ftype PythonScript=C:\Python312\python.exe "%1" %* +assoc .py=PythonScript +PATH=C:\Python312;C:\Python312\Scripts;%PATH% +setx PATH "C:\Python312;C:\Python312\Scripts;%PATH%" + +python -m pip install --upgrade wheel pip +pip install -r F:\src\requirements.txt + +setup\nsis-install.exe /S + +powershell -command "Expand-Archive setup\NSIS_Simple_Firewall_Plugin_1.20.zip nsis-fp" +copy nsis-fp\SimpleFC.dll "C:\Program Files (x86)\NSIS\Plugins\x86-ansi\" +copy nsis-fp\SimpleFC.dll "C:\Program Files (x86)\NSIS\Plugins\x86-unicode\" +rmdir nsis-fp /s /q + +rmdir setup /s /q