AI Update: معماری امنیتی و ثبت در دیتابیس مورد تأیید است، لطفاً پیادهس
This commit is contained in:
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+84
@@ -0,0 +1,84 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: paramiko
|
||||||
|
Version: 5.0.0
|
||||||
|
Summary: SSH2 protocol library
|
||||||
|
Author-email: Jeff Forcier <jeff@bitprophet.org>
|
||||||
|
License-Expression: LGPL-2.1
|
||||||
|
Project-URL: Docs, https://docs.paramiko.org
|
||||||
|
Project-URL: Source, https://github.com/paramiko/paramiko
|
||||||
|
Project-URL: Changelog, https://www.paramiko.org/changelog.html
|
||||||
|
Project-URL: CI, https://app.circleci.com/pipelines/github/paramiko/paramiko
|
||||||
|
Project-URL: Issues, https://github.com/paramiko/paramiko/issues
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: Internet
|
||||||
|
Classifier: Topic :: Security :: Cryptography
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: bcrypt>=3.2
|
||||||
|
Requires-Dist: cryptography>=3.3
|
||||||
|
Requires-Dist: invoke>=2.0
|
||||||
|
Requires-Dist: pynacl>=1.5
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
|version| |python| |license| |ci| |coverage|
|
||||||
|
|
||||||
|
.. |version| image:: https://img.shields.io/pypi/v/paramiko
|
||||||
|
:target: https://pypi.org/project/paramiko/
|
||||||
|
:alt: PyPI - Package Version
|
||||||
|
.. |python| image:: https://img.shields.io/pypi/pyversions/paramiko
|
||||||
|
:target: https://pypi.org/project/paramiko/
|
||||||
|
:alt: PyPI - Python Version
|
||||||
|
.. |license| image:: https://img.shields.io/pypi/l/paramiko
|
||||||
|
:target: https://github.com/paramiko/paramiko/blob/main/LICENSE
|
||||||
|
:alt: PyPI - License
|
||||||
|
.. |ci| image:: https://img.shields.io/circleci/build/github/paramiko/paramiko/main
|
||||||
|
:target: https://app.circleci.com/pipelines/github/paramiko/paramiko
|
||||||
|
:alt: CircleCI
|
||||||
|
.. |coverage| image:: https://img.shields.io/codecov/c/gh/paramiko/paramiko
|
||||||
|
:target: https://app.codecov.io/gh/paramiko/paramiko
|
||||||
|
:alt: Codecov
|
||||||
|
|
||||||
|
Welcome to Paramiko!
|
||||||
|
====================
|
||||||
|
|
||||||
|
Paramiko is a pure-Python [#]_ implementation of the SSHv2 protocol [#]_,
|
||||||
|
providing both client and server functionality. It provides the foundation for
|
||||||
|
the high-level SSH library `Fabric <https://fabfile.org>`_, which is what we
|
||||||
|
recommend you use for common client use-cases such as running remote shell
|
||||||
|
commands or transferring files.
|
||||||
|
|
||||||
|
Direct use of Paramiko itself is only intended for users who need
|
||||||
|
advanced/low-level primitives or want to run an in-Python sshd.
|
||||||
|
|
||||||
|
For installation information, changelogs, FAQs and similar, please visit `our
|
||||||
|
main project website <https://paramiko.org>`_; for API details, see `the
|
||||||
|
versioned docs <https://docs.paramiko.org>`_. Additionally, the project
|
||||||
|
maintainer keeps a `roadmap <http://bitprophet.org/projects#roadmap>`_ on his
|
||||||
|
personal site.
|
||||||
|
|
||||||
|
.. [#]
|
||||||
|
Paramiko relies on `cryptography <https://cryptography.io>`_ for crypto
|
||||||
|
functionality, which makes use of C and Rust extensions but has many
|
||||||
|
precompiled options available. See `our installation page
|
||||||
|
<https://www.paramiko.org/installing.html>`_ for details.
|
||||||
|
|
||||||
|
.. [#]
|
||||||
|
OpenSSH's RFC specification page is a fantastic resource and collection of
|
||||||
|
links that we won't bother replicating here:
|
||||||
|
https://www.openssh.com/specs.html
|
||||||
|
|
||||||
|
OpenSSH itself also happens to be our primary reference implementation:
|
||||||
|
when in doubt, we consult how they do things, unless there are good reasons
|
||||||
|
not to. There are always some gaps, but we do our best to reconcile them
|
||||||
|
when possible.
|
||||||
Binary file not shown.
+275
@@ -0,0 +1,275 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: PyNaCl
|
||||||
|
Version: 1.6.2
|
||||||
|
Summary: Python binding to the Networking and Cryptography (NaCl) library
|
||||||
|
Home-page: https://github.com/pyca/pynacl/
|
||||||
|
Author-email: The PyNaCl developers <cryptography-dev@python.org>
|
||||||
|
License: Apache-2.0
|
||||||
|
Project-URL: Homepage, https://github.com/pyca/pynacl
|
||||||
|
Project-URL: Bug Tracker, https://github.com/pyca/pynacl/issues
|
||||||
|
Project-URL: Documentation, https://pynacl.readthedocs.io
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
License-File: LICENSE
|
||||||
|
License-File: licenses/LICENSE.libsodium.txt
|
||||||
|
Requires-Dist: cffi>=1.4.1; platform_python_implementation != "PyPy" and python_version < "3.9"
|
||||||
|
Requires-Dist: cffi>=2.0.0; platform_python_implementation != "PyPy" and python_version >= "3.9"
|
||||||
|
Provides-Extra: tests
|
||||||
|
Requires-Dist: pytest>=7.4.0; extra == "tests"
|
||||||
|
Requires-Dist: pytest-cov>=2.10.1; extra == "tests"
|
||||||
|
Requires-Dist: pytest-xdist>=3.5.0; extra == "tests"
|
||||||
|
Requires-Dist: hypothesis>=3.27.0; extra == "tests"
|
||||||
|
Provides-Extra: docs
|
||||||
|
Requires-Dist: sphinx<7; extra == "docs"
|
||||||
|
Requires-Dist: sphinx_rtd_theme; extra == "docs"
|
||||||
|
Dynamic: description
|
||||||
|
Dynamic: home-page
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
===============================================
|
||||||
|
PyNaCl: Python binding to the libsodium library
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/pynacl.svg
|
||||||
|
:target: https://pypi.org/project/PyNaCl/
|
||||||
|
:alt: Latest Version
|
||||||
|
|
||||||
|
PyNaCl is a Python binding to `libsodium`_, which is a fork of the
|
||||||
|
`Networking and Cryptography library`_. These libraries have a stated goal of
|
||||||
|
improving usability, security and speed. It supports Python 3.8+ as well as
|
||||||
|
PyPy 3.
|
||||||
|
|
||||||
|
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||||
|
.. _Networking and Cryptography library: https://nacl.cr.yp.to/
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Digital signatures
|
||||||
|
* Secret-key encryption
|
||||||
|
* Public-key encryption
|
||||||
|
* Hashing and message authentication
|
||||||
|
* Password based key derivation and password hashing
|
||||||
|
|
||||||
|
`Changelog`_
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. _Changelog: https://pynacl.readthedocs.io/en/latest/changelog/
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Binary wheel install
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
PyNaCl ships as a binary wheel on macOS, Windows and Linux ``manylinux1`` [#many]_ ,
|
||||||
|
so all dependencies are included. Make sure you have an up-to-date pip
|
||||||
|
and run:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install pynacl
|
||||||
|
|
||||||
|
Faster wheel build
|
||||||
|
------------------
|
||||||
|
|
||||||
|
You can define the environment variable ``LIBSODIUM_MAKE_ARGS`` to pass arguments to ``make``
|
||||||
|
and enable `parallelization`_:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ LIBSODIUM_MAKE_ARGS=-j4 pip install pynacl
|
||||||
|
|
||||||
|
Linux source build
|
||||||
|
------------------
|
||||||
|
|
||||||
|
PyNaCl relies on `libsodium`_, a portable C library. A copy is bundled
|
||||||
|
with PyNaCl so to install you can run:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install pynacl
|
||||||
|
|
||||||
|
If you'd prefer to use the version of ``libsodium`` provided by your
|
||||||
|
distribution, you can disable the bundled copy during install by running:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ SODIUM_INSTALL=system pip install pynacl
|
||||||
|
|
||||||
|
.. warning:: Usage of the legacy ``easy_install`` command provided by setuptools
|
||||||
|
is generally discouraged, and is completely unsupported in PyNaCl's case.
|
||||||
|
|
||||||
|
.. _parallelization: https://www.gnu.org/software/make/manual/html_node/Parallel.html
|
||||||
|
|
||||||
|
.. _libsodium: https://github.com/jedisct1/libsodium
|
||||||
|
|
||||||
|
.. [#many] `manylinux1 wheels <https://www.python.org/dev/peps/pep-0513/>`_
|
||||||
|
are built on a baseline linux environment based on Centos 5.11
|
||||||
|
and should work on most x86 and x86_64 glibc based linux environments.
|
||||||
|
|
||||||
|
GNU Make (gmake) may be required for newer versions of PyNaCl. You can
|
||||||
|
set the binary to use by setting the ``MAKE`` environment variable:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ MAKE=gmake pip install pynacl
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
1.6.2 (2026-01-01)
|
||||||
|
------------------
|
||||||
|
* Updated ``libsodium`` to 1.0.20-stable (2025-12-31 build) to resolve
|
||||||
|
``CVE-2025-69277``.
|
||||||
|
|
||||||
|
1.6.1 (2025-11-10)
|
||||||
|
------------------
|
||||||
|
* The ``MAKE`` environment variable can now be used to specify the ``make``
|
||||||
|
binary that should be used in the build process.
|
||||||
|
|
||||||
|
1.6.0 (2025-09-11)
|
||||||
|
------------------
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** Removed support for Python 3.6 and 3.7.
|
||||||
|
* Added support for the low level AEAD AES bindings.
|
||||||
|
* Added support for ``crypto_core_ed25519_from_uniform``.
|
||||||
|
* Update ``libsodium`` to 1.0.20-stable (2025-08-27 build).
|
||||||
|
* Added support for free-threaded Python 3.14.
|
||||||
|
* Added support for Windows on ARM wheels.
|
||||||
|
|
||||||
|
1.5.0 (2022-01-07)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** Removed support for Python 2.7 and Python 3.5.
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** We no longer distribute ``manylinux1``
|
||||||
|
wheels.
|
||||||
|
* Added ``manylinux2014``, ``manylinux_2_24``, ``musllinux``, and macOS
|
||||||
|
``universal2`` wheels (the latter supports macOS ``arm64``).
|
||||||
|
* Update ``libsodium`` to 1.0.18-stable (July 25, 2021 release).
|
||||||
|
* Add inline type hints.
|
||||||
|
|
||||||
|
1.4.0 (2020-05-25)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update ``libsodium`` to 1.0.18.
|
||||||
|
* **BACKWARDS INCOMPATIBLE:** We no longer distribute 32-bit ``manylinux1``
|
||||||
|
wheels. Continuing to produce them was a maintenance burden.
|
||||||
|
* Added support for Python 3.8, and removed support for Python 3.4.
|
||||||
|
* Add low level bindings for extracting the seed and the public key
|
||||||
|
from crypto_sign_ed25519 secret key
|
||||||
|
* Add low level bindings for deterministic random generation.
|
||||||
|
* Add ``wheel`` and ``setuptools`` setup_requirements in ``setup.py`` (#485)
|
||||||
|
* Fix checks on very slow builders (#481, #495)
|
||||||
|
* Add low-level bindings to ed25519 arithmetic functions
|
||||||
|
* Update low-level blake2b state implementation
|
||||||
|
* Fix wrong short-input behavior of SealedBox.decrypt() (#517)
|
||||||
|
* Raise CryptPrefixError exception instead of InvalidkeyError when trying
|
||||||
|
to check a password against a verifier stored in a unknown format (#519)
|
||||||
|
* Add support for minimal builds of libsodium. Trying to call functions
|
||||||
|
not available in a minimal build will raise an UnavailableError
|
||||||
|
exception. To compile a minimal build of the bundled libsodium, set
|
||||||
|
the SODIUM_INSTALL_MINIMAL environment variable to any non-empty
|
||||||
|
string (e.g. ``SODIUM_INSTALL_MINIMAL=1``) for setup.
|
||||||
|
|
||||||
|
1.3.0 2018-09-26
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* Added support for Python 3.7.
|
||||||
|
* Update ``libsodium`` to 1.0.16.
|
||||||
|
* Run and test all code examples in PyNaCl docs through sphinx's
|
||||||
|
doctest builder.
|
||||||
|
* Add low-level bindings for chacha20-poly1305 AEAD constructions.
|
||||||
|
* Add low-level bindings for the chacha20-poly1305 secretstream constructions.
|
||||||
|
* Add low-level bindings for ed25519ph pre-hashed signing construction.
|
||||||
|
* Add low-level bindings for constant-time increment and addition
|
||||||
|
on fixed-precision big integers represented as little-endian
|
||||||
|
byte sequences.
|
||||||
|
* Add low-level bindings for the ISO/IEC 7816-4 compatible padding API.
|
||||||
|
* Add low-level bindings for libsodium's crypto_kx... key exchange
|
||||||
|
construction.
|
||||||
|
* Set hypothesis deadline to None in tests/test_pwhash.py to avoid
|
||||||
|
incorrect test failures on slower processor architectures. GitHub
|
||||||
|
issue #370
|
||||||
|
|
||||||
|
1.2.1 - 2017-12-04
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update hypothesis minimum allowed version.
|
||||||
|
* Infrastructure: add proper configuration for readthedocs builder
|
||||||
|
runtime environment.
|
||||||
|
|
||||||
|
1.2.0 - 2017-11-01
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Update ``libsodium`` to 1.0.15.
|
||||||
|
* Infrastructure: add jenkins support for automatic build of
|
||||||
|
``manylinux1`` binary wheels
|
||||||
|
* Added support for ``SealedBox`` construction.
|
||||||
|
* Added support for ``argon2i`` and ``argon2id`` password hashing constructs
|
||||||
|
and restructured high-level password hashing implementation to expose
|
||||||
|
the same interface for all hashers.
|
||||||
|
* Added support for 128 bit ``siphashx24`` variant of ``siphash24``.
|
||||||
|
* Added support for ``from_seed`` APIs for X25519 key pair generation.
|
||||||
|
* Dropped support for Python 3.3.
|
||||||
|
|
||||||
|
1.1.2 - 2017-03-31
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* reorder link time library search path when using bundled
|
||||||
|
libsodium
|
||||||
|
|
||||||
|
1.1.1 - 2017-03-15
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Fixed a circular import bug in ``nacl.utils``.
|
||||||
|
|
||||||
|
1.1.0 - 2017-03-14
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Dropped support for Python 2.6.
|
||||||
|
* Added ``shared_key()`` method on ``Box``.
|
||||||
|
* You can now pass ``None`` to ``nonce`` when encrypting with ``Box`` or
|
||||||
|
``SecretBox`` and it will automatically generate a random nonce.
|
||||||
|
* Added support for ``siphash24``.
|
||||||
|
* Added support for ``blake2b``.
|
||||||
|
* Added support for ``scrypt``.
|
||||||
|
* Update ``libsodium`` to 1.0.11.
|
||||||
|
* Default to the bundled ``libsodium`` when compiling.
|
||||||
|
* All raised exceptions are defined mixing-in
|
||||||
|
``nacl.exceptions.CryptoError``
|
||||||
|
|
||||||
|
1.0.1 - 2016-01-24
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* Fix an issue with absolute paths that prevented the creation of wheels.
|
||||||
|
|
||||||
|
1.0 - 2016-01-23
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* PyNaCl has been ported to use the new APIs available in cffi 1.0+.
|
||||||
|
Due to this change we no longer support PyPy releases older than 2.6.
|
||||||
|
* Python 3.2 support has been dropped.
|
||||||
|
* Functions to convert between Ed25519 and Curve25519 keys have been added.
|
||||||
|
|
||||||
|
0.3.0 - 2015-03-04
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* The low-level API (`nacl.c.*`) has been changed to match the
|
||||||
|
upstream NaCl C/C++ conventions (as well as those of other NaCl bindings).
|
||||||
|
The order of arguments and return values has changed significantly. To
|
||||||
|
avoid silent failures, `nacl.c` has been removed, and replaced with
|
||||||
|
`nacl.bindings` (with the new argument ordering). If you have code which
|
||||||
|
calls these functions (e.g. `nacl.c.crypto_box_keypair()`), you must review
|
||||||
|
the new docstrings and update your code/imports to match the new
|
||||||
|
conventions.
|
||||||
Binary file not shown.
+74
@@ -0,0 +1,74 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: invoke
|
||||||
|
Version: 3.0.3
|
||||||
|
Summary: Pythonic task execution
|
||||||
|
Author-email: Jeff Forcier <jeff@bitprophet.org>
|
||||||
|
License-Expression: BSD-2-Clause
|
||||||
|
Project-URL: Docs, https://docs.pyinvoke.org
|
||||||
|
Project-URL: Source, https://github.com/pyinvoke/invoke
|
||||||
|
Project-URL: Changelog, https://www.pyinvoke.org/changelog.html
|
||||||
|
Project-URL: CI, https://app.circleci.com/pipelines/github/pyinvoke/invoke
|
||||||
|
Project-URL: Issues, https://github.com/pyinvoke/invoke/issues
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Console
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: Operating System :: Unix
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Topic :: Software Development
|
||||||
|
Classifier: Topic :: Software Development :: Build Tools
|
||||||
|
Classifier: Topic :: Software Development :: Libraries
|
||||||
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Classifier: Topic :: System :: Software Distribution
|
||||||
|
Classifier: Topic :: System :: Systems Administration
|
||||||
|
Requires-Python: >=3.9
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
|version| |python| |license| |ci| |coverage|
|
||||||
|
|
||||||
|
.. |version| image:: https://img.shields.io/pypi/v/invoke
|
||||||
|
:target: https://pypi.org/project/invoke/
|
||||||
|
:alt: PyPI - Package Version
|
||||||
|
.. |python| image:: https://img.shields.io/pypi/pyversions/invoke
|
||||||
|
:target: https://pypi.org/project/invoke/
|
||||||
|
:alt: PyPI - Python Version
|
||||||
|
.. |license| image:: https://img.shields.io/pypi/l/invoke
|
||||||
|
:target: https://github.com/pyinvoke/invoke/blob/main/LICENSE
|
||||||
|
:alt: PyPI - License
|
||||||
|
.. |ci| image:: https://img.shields.io/circleci/build/github/pyinvoke/invoke/main
|
||||||
|
:target: https://app.circleci.com/pipelines/github/pyinvoke/invoke
|
||||||
|
:alt: CircleCI
|
||||||
|
.. |coverage| image:: https://img.shields.io/codecov/c/gh/pyinvoke/invoke
|
||||||
|
:target: https://app.codecov.io/gh/pyinvoke/invoke
|
||||||
|
:alt: Codecov
|
||||||
|
|
||||||
|
Welcome to Invoke!
|
||||||
|
==================
|
||||||
|
|
||||||
|
Invoke is a Python library for managing shell-oriented subprocesses and
|
||||||
|
organizing executable Python code into CLI-invokable tasks. It draws
|
||||||
|
inspiration from various sources (``make``/``rake``, Fabric 1.x, etc) to arrive
|
||||||
|
at a powerful & clean feature set.
|
||||||
|
|
||||||
|
To find out what's new in this version of Invoke, please see `the changelog
|
||||||
|
<https://pyinvoke.org/changelog.html#{}>`_.
|
||||||
|
|
||||||
|
For a high level introduction, including example code, please see `our main
|
||||||
|
project website <https://pyinvoke.org>`_; or for detailed API docs, see `the
|
||||||
|
versioned API website <https://docs.pyinvoke.org>`_.
|
||||||
|
|
||||||
|
The project maintainer keeps a `roadmap
|
||||||
|
<https://bitprophet.org/projects#roadmap>`_ on his website.
|
||||||
Binary file not shown.
+67
@@ -0,0 +1,67 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: cffi
|
||||||
|
Version: 2.1.1
|
||||||
|
Summary: Foreign Function Interface for Python calling C code.
|
||||||
|
Author: Armin Rigo, Maciej Fijalkowski
|
||||||
|
Maintainer: Matt Davis, Matt Clay
|
||||||
|
License-Expression: MIT-0
|
||||||
|
Project-URL: Documentation, https://cffi.readthedocs.io/
|
||||||
|
Project-URL: Changelog, https://cffi.readthedocs.io/en/latest/whatsnew.html
|
||||||
|
Project-URL: Download, https://github.com/python-cffi/cffi/releases
|
||||||
|
Project-URL: Contact, https://groups.google.com/forum/#!forum/python-cffi
|
||||||
|
Project-URL: Source Code, https://github.com/python-cffi/cffi
|
||||||
|
Project-URL: Issue Tracker, https://github.com/python-cffi/cffi/issues
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Programming Language :: Python :: 3.15
|
||||||
|
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Requires-Python: >=3.10
|
||||||
|
Description-Content-Type: text/markdown
|
||||||
|
License-File: LICENSE
|
||||||
|
Requires-Dist: pycparser; implementation_name != "PyPy"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
[](https://github.com/python-cffi/cffi/actions/workflows/ci.yaml?query=branch%3Amain++)
|
||||||
|
[](https://pypi.org/project/cffi)
|
||||||
|
[][Documentation]
|
||||||
|
|
||||||
|
|
||||||
|
CFFI
|
||||||
|
====
|
||||||
|
|
||||||
|
Foreign Function Interface for Python calling C code.
|
||||||
|
|
||||||
|
Please see the [Documentation] or uncompiled in the `doc/` subdirectory.
|
||||||
|
|
||||||
|
Download
|
||||||
|
--------
|
||||||
|
|
||||||
|
[Download page](https://github.com/python-cffi/cffi/releases)
|
||||||
|
|
||||||
|
Source Code
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Source code is publicly available on
|
||||||
|
[GitHub](https://github.com/python-cffi/cffi).
|
||||||
|
|
||||||
|
Contact
|
||||||
|
-------
|
||||||
|
|
||||||
|
[Mailing list](https://groups.google.com/forum/#!forum/python-cffi)
|
||||||
|
|
||||||
|
Testing/development tips
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
After `git clone` or `wget && tar`, we will get a directory called `cffi` or `cffi-x.x.x`. We call it `repo-directory`. To run tests under CPython, run the following in the `repo-directory`:
|
||||||
|
|
||||||
|
pip install pytest
|
||||||
|
pip install -e . # editable install of CFFI for local development
|
||||||
|
pytest src/c/ testing/
|
||||||
|
|
||||||
|
[Documentation]: http://cffi.readthedocs.org/
|
||||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+343
@@ -0,0 +1,343 @@
|
|||||||
|
Metadata-Version: 2.4
|
||||||
|
Name: bcrypt
|
||||||
|
Version: 5.0.0
|
||||||
|
Summary: Modern password hashing for your software and your servers
|
||||||
|
Author-email: The Python Cryptographic Authority developers <cryptography-dev@python.org>
|
||||||
|
License: Apache-2.0
|
||||||
|
Project-URL: homepage, https://github.com/pyca/bcrypt/
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: License :: OSI Approved :: Apache Software License
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3 :: Only
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Classifier: Programming Language :: Python :: 3.14
|
||||||
|
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
Description-Content-Type: text/x-rst
|
||||||
|
License-File: LICENSE
|
||||||
|
Provides-Extra: tests
|
||||||
|
Requires-Dist: pytest!=3.3.0,>=3.2.1; extra == "tests"
|
||||||
|
Provides-Extra: typecheck
|
||||||
|
Requires-Dist: mypy; extra == "typecheck"
|
||||||
|
Dynamic: license-file
|
||||||
|
|
||||||
|
bcrypt
|
||||||
|
======
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/bcrypt.svg
|
||||||
|
:target: https://pypi.org/project/bcrypt/
|
||||||
|
:alt: Latest Version
|
||||||
|
|
||||||
|
.. image:: https://github.com/pyca/bcrypt/workflows/CI/badge.svg?branch=main
|
||||||
|
:target: https://github.com/pyca/bcrypt/actions?query=workflow%3ACI+branch%3Amain
|
||||||
|
|
||||||
|
Acceptable password hashing for your software and your servers (but you should
|
||||||
|
really use argon2id or scrypt)
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
To install bcrypt, simply:
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
$ pip install bcrypt
|
||||||
|
|
||||||
|
Note that bcrypt should build very easily on Linux provided you have a C
|
||||||
|
compiler and a Rust compiler (the minimum supported Rust version is 1.56.0).
|
||||||
|
|
||||||
|
For Debian and Ubuntu, the following command will ensure that the required dependencies are installed:
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
$ sudo apt-get install build-essential cargo
|
||||||
|
|
||||||
|
For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed:
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
$ sudo yum install gcc cargo
|
||||||
|
|
||||||
|
For Alpine, the following command will ensure that the required dependencies are installed:
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
$ apk add --update musl-dev gcc cargo
|
||||||
|
|
||||||
|
|
||||||
|
Alternatives
|
||||||
|
============
|
||||||
|
|
||||||
|
While bcrypt remains an acceptable choice for password storage, depending on your specific use case you may also want to consider using scrypt (either via `standard library`_ or `cryptography`_) or argon2id via `argon2_cffi`_.
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
5.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Bumped MSRV to 1.74.
|
||||||
|
* Added support for Python 3.14 and free-threaded Python 3.14.
|
||||||
|
* Added support for Windows on ARM.
|
||||||
|
* Passing ``hashpw`` a password longer than 72 bytes now raises a
|
||||||
|
``ValueError``. Previously the password was silently truncated, following the
|
||||||
|
behavior of the original OpenBSD ``bcrypt`` implementation.
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Dropped support for Python 3.7.
|
||||||
|
* We now support free-threaded Python 3.13.
|
||||||
|
* We now support PyPy 3.11.
|
||||||
|
* We now publish wheels for free-threaded Python 3.13, for PyPy 3.11 on
|
||||||
|
``manylinux``, and for ARMv7l on ``manylinux``.
|
||||||
|
|
||||||
|
4.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Bump Rust dependency versions - this should resolve crashes on Python 3.13
|
||||||
|
free-threaded builds.
|
||||||
|
* We no longer build ``manylinux`` wheels for PyPy 3.9.
|
||||||
|
|
||||||
|
4.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Bump Rust dependency versions
|
||||||
|
* Removed the ``BCRYPT_ALLOW_RUST_163`` environment variable.
|
||||||
|
|
||||||
|
4.1.3
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Bump Rust dependency versions
|
||||||
|
|
||||||
|
4.1.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Publish both ``py37`` and ``py39`` wheels. This should resolve some errors
|
||||||
|
relating to initializing a module multiple times per process.
|
||||||
|
|
||||||
|
4.1.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Fixed the type signature on the ``kdf`` method.
|
||||||
|
* Fixed packaging bug on Windows.
|
||||||
|
* Fixed incompatibility with passlib package detection assumptions.
|
||||||
|
|
||||||
|
4.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Dropped support for Python 3.6.
|
||||||
|
* Bumped MSRV to 1.64. (Note: Rust 1.63 can be used by setting the ``BCRYPT_ALLOW_RUST_163`` environment variable)
|
||||||
|
|
||||||
|
4.0.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* We now build PyPy ``manylinux`` wheels.
|
||||||
|
* Fixed a bug where passing an invalid ``salt`` to ``checkpw`` could result in
|
||||||
|
a ``pyo3_runtime.PanicException``. It now correctly raises a ``ValueError``.
|
||||||
|
|
||||||
|
4.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* ``bcrypt`` is now implemented in Rust. Users building from source will need
|
||||||
|
to have a Rust compiler available. Nothing will change for users downloading
|
||||||
|
wheels.
|
||||||
|
* We no longer ship ``manylinux2010`` wheels. Users should upgrade to the latest
|
||||||
|
``pip`` to ensure this doesn’t cause issues downloading wheels on their
|
||||||
|
platform. We now ship ``manylinux_2_28`` wheels for users on new enough platforms.
|
||||||
|
* ``NUL`` bytes are now allowed in inputs.
|
||||||
|
|
||||||
|
|
||||||
|
3.2.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Fixed packaging of ``py.typed`` files in wheels so that ``mypy`` works.
|
||||||
|
|
||||||
|
3.2.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added support for compilation on z/OS
|
||||||
|
* The next release of ``bcrypt`` with be 4.0 and it will require Rust at
|
||||||
|
compile time, for users building from source. There will be no additional
|
||||||
|
requirement for users who are installing from wheels. Users on most
|
||||||
|
platforms will be able to obtain a wheel by making sure they have an up to
|
||||||
|
date ``pip``. The minimum supported Rust version will be 1.56.0.
|
||||||
|
* This will be the final release for which we ship ``manylinux2010`` wheels.
|
||||||
|
Going forward the minimum supported manylinux ABI for our wheels will be
|
||||||
|
``manylinux2014``. The vast majority of users will continue to receive
|
||||||
|
``manylinux`` wheels provided they have an up to date ``pip``.
|
||||||
|
|
||||||
|
|
||||||
|
3.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added typehints for library functions.
|
||||||
|
* Dropped support for Python versions less than 3.6 (2.7, 3.4, 3.5).
|
||||||
|
* Shipped ``abi3`` Windows wheels (requires pip >= 20).
|
||||||
|
|
||||||
|
3.1.7
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Set a ``setuptools`` lower bound for PEP517 wheel building.
|
||||||
|
* We no longer distribute 32-bit ``manylinux1`` wheels. Continuing to produce
|
||||||
|
them was a maintenance burden.
|
||||||
|
|
||||||
|
3.1.6
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added support for compilation on Haiku.
|
||||||
|
|
||||||
|
3.1.5
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added support for compilation on AIX.
|
||||||
|
* Dropped Python 2.6 and 3.3 support.
|
||||||
|
* Switched to using ``abi3`` wheels for Python 3. If you are not getting a
|
||||||
|
wheel on a compatible platform please upgrade your ``pip`` version.
|
||||||
|
|
||||||
|
3.1.4
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Fixed compilation with mingw and on illumos.
|
||||||
|
|
||||||
|
3.1.3
|
||||||
|
-----
|
||||||
|
* Fixed a compilation issue on Solaris.
|
||||||
|
* Added a warning when using too few rounds with ``kdf``.
|
||||||
|
|
||||||
|
3.1.2
|
||||||
|
-----
|
||||||
|
* Fixed a compile issue affecting big endian platforms.
|
||||||
|
* Fixed invalid escape sequence warnings on Python 3.6.
|
||||||
|
* Fixed building in non-UTF8 environments on Python 2.
|
||||||
|
|
||||||
|
3.1.1
|
||||||
|
-----
|
||||||
|
* Resolved a ``UserWarning`` when used with ``cffi`` 1.8.3.
|
||||||
|
|
||||||
|
3.1.0
|
||||||
|
-----
|
||||||
|
* Added support for ``checkpw``, a convenience method for verifying a password.
|
||||||
|
* Ensure that you get a ``$2y$`` hash when you input a ``$2y$`` salt.
|
||||||
|
* Fixed a regression where ``$2a`` hashes were vulnerable to a wraparound bug.
|
||||||
|
* Fixed compilation under Alpine Linux.
|
||||||
|
|
||||||
|
3.0.0
|
||||||
|
-----
|
||||||
|
* Switched the C backend to code obtained from the OpenBSD project rather than
|
||||||
|
openwall.
|
||||||
|
* Added support for ``bcrypt_pbkdf`` via the ``kdf`` function.
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
-----
|
||||||
|
* Added support for an adjustible prefix when calling ``gensalt``.
|
||||||
|
* Switched to CFFI 1.0+
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
Password Hashing
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Hashing and then later checking that a password matches the previous hashed
|
||||||
|
password is very simple:
|
||||||
|
|
||||||
|
.. code:: pycon
|
||||||
|
|
||||||
|
>>> import bcrypt
|
||||||
|
>>> password = b"super secret password"
|
||||||
|
>>> # Hash a password for the first time, with a randomly-generated salt
|
||||||
|
>>> hashed = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
|
>>> # Check that an unhashed password matches one that has previously been
|
||||||
|
>>> # hashed
|
||||||
|
>>> if bcrypt.checkpw(password, hashed):
|
||||||
|
... print("It Matches!")
|
||||||
|
... else:
|
||||||
|
... print("It Does not Match :(")
|
||||||
|
|
||||||
|
KDF
|
||||||
|
~~~
|
||||||
|
|
||||||
|
As of 3.0.0 ``bcrypt`` now offers a ``kdf`` function which does ``bcrypt_pbkdf``.
|
||||||
|
This KDF is used in OpenSSH's newer encrypted private key format.
|
||||||
|
|
||||||
|
.. code:: pycon
|
||||||
|
|
||||||
|
>>> import bcrypt
|
||||||
|
>>> key = bcrypt.kdf(
|
||||||
|
... password=b'password',
|
||||||
|
... salt=b'salt',
|
||||||
|
... desired_key_bytes=32,
|
||||||
|
... rounds=100)
|
||||||
|
|
||||||
|
|
||||||
|
Adjustable Work Factor
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
One of bcrypt's features is an adjustable logarithmic work factor. To adjust
|
||||||
|
the work factor merely pass the desired number of rounds to
|
||||||
|
``bcrypt.gensalt(rounds=12)`` which defaults to 12):
|
||||||
|
|
||||||
|
.. code:: pycon
|
||||||
|
|
||||||
|
>>> import bcrypt
|
||||||
|
>>> password = b"super secret password"
|
||||||
|
>>> # Hash a password for the first time, with a certain number of rounds
|
||||||
|
>>> hashed = bcrypt.hashpw(password, bcrypt.gensalt(14))
|
||||||
|
>>> # Check that a unhashed password matches one that has previously been
|
||||||
|
>>> # hashed
|
||||||
|
>>> if bcrypt.checkpw(password, hashed):
|
||||||
|
... print("It Matches!")
|
||||||
|
... else:
|
||||||
|
... print("It Does not Match :(")
|
||||||
|
|
||||||
|
|
||||||
|
Adjustable Prefix
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Another one of bcrypt's features is an adjustable prefix to let you define what
|
||||||
|
libraries you'll remain compatible with. To adjust this, pass either ``2a`` or
|
||||||
|
``2b`` (the default) to ``bcrypt.gensalt(prefix=b"2b")`` as a bytes object.
|
||||||
|
|
||||||
|
As of 3.0.0 the ``$2y$`` prefix is still supported in ``hashpw`` but deprecated.
|
||||||
|
|
||||||
|
Maximum Password Length
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The bcrypt algorithm only handles passwords up to 72 characters, any characters
|
||||||
|
beyond that are ignored. To work around this, a common approach is to hash a
|
||||||
|
password with a cryptographic hash (such as ``sha256``) and then base64
|
||||||
|
encode it to prevent NULL byte problems before hashing the result with
|
||||||
|
``bcrypt``:
|
||||||
|
|
||||||
|
.. code:: pycon
|
||||||
|
|
||||||
|
>>> password = b"an incredibly long password" * 10
|
||||||
|
>>> hashed = bcrypt.hashpw(
|
||||||
|
... base64.b64encode(hashlib.sha256(password).digest()),
|
||||||
|
... bcrypt.gensalt()
|
||||||
|
... )
|
||||||
|
|
||||||
|
Compatibility
|
||||||
|
-------------
|
||||||
|
|
||||||
|
This library should be compatible with py-bcrypt and it will run on Python
|
||||||
|
3.8+ (including free-threaded builds), and PyPy 3.
|
||||||
|
|
||||||
|
Security
|
||||||
|
--------
|
||||||
|
|
||||||
|
``bcrypt`` follows the `same security policy as cryptography`_, if you
|
||||||
|
identify a vulnerability, we ask you to contact us privately.
|
||||||
|
|
||||||
|
.. _`same security policy as cryptography`: https://cryptography.io/en/latest/security.html
|
||||||
|
.. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt
|
||||||
|
.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io
|
||||||
|
.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt
|
||||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -130,6 +130,12 @@ class Project:
|
|||||||
ftp_password: Optional[str] = None
|
ftp_password: Optional[str] = None
|
||||||
ftp_path: str = "/"
|
ftp_path: str = "/"
|
||||||
ftp_tls: bool = False
|
ftp_tls: bool = False
|
||||||
|
ssh_host: Optional[str] = None
|
||||||
|
ssh_port: int = 22
|
||||||
|
ssh_user: Optional[str] = None
|
||||||
|
ssh_password: Optional[str] = None
|
||||||
|
ssh_key: Optional[str] = None
|
||||||
|
ssh_path: str = "/"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Session:
|
class Session:
|
||||||
@@ -272,6 +278,12 @@ class SessionManager:
|
|||||||
"ftp_password",
|
"ftp_password",
|
||||||
"ftp_path",
|
"ftp_path",
|
||||||
"ftp_tls",
|
"ftp_tls",
|
||||||
|
"ssh_host",
|
||||||
|
"ssh_port",
|
||||||
|
"ssh_user",
|
||||||
|
"ssh_password",
|
||||||
|
"ssh_key",
|
||||||
|
"ssh_path",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if "name" not in clean_p:
|
if "name" not in clean_p:
|
||||||
@@ -367,6 +379,12 @@ class SessionManager:
|
|||||||
"ftp_password": p_obj.ftp_password,
|
"ftp_password": p_obj.ftp_password,
|
||||||
"ftp_path": p_obj.ftp_path,
|
"ftp_path": p_obj.ftp_path,
|
||||||
"ftp_tls": p_obj.ftp_tls,
|
"ftp_tls": p_obj.ftp_tls,
|
||||||
|
"ssh_host": p_obj.ssh_host,
|
||||||
|
"ssh_port": p_obj.ssh_port,
|
||||||
|
"ssh_user": p_obj.ssh_user,
|
||||||
|
"ssh_password": p_obj.ssh_password,
|
||||||
|
"ssh_key": p_obj.ssh_key,
|
||||||
|
"ssh_path": p_obj.ssh_path,
|
||||||
}
|
}
|
||||||
curr = self.get_current_project(sess.chat_id)
|
curr = self.get_current_project(sess.chat_id)
|
||||||
data[str(k)] = {
|
data[str(k)] = {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from formatters import (
|
|||||||
format_git_commits_page,
|
format_git_commits_page,
|
||||||
format_commit_detail_view,
|
format_commit_detail_view,
|
||||||
format_ftp_info,
|
format_ftp_info,
|
||||||
|
format_ssh_info,
|
||||||
split_message,
|
split_message,
|
||||||
escape_html,
|
escape_html,
|
||||||
)
|
)
|
||||||
@@ -69,6 +70,8 @@ from invite_manager import (
|
|||||||
from backup_manager import backup_manager
|
from backup_manager import backup_manager
|
||||||
from git_manager import git_manager
|
from git_manager import git_manager
|
||||||
from ftp_manager import ftp_manager
|
from ftp_manager import ftp_manager
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
from remote_audit import remote_audit
|
||||||
from sys_monitor import render_server_hardware_report
|
from sys_monitor import render_server_hardware_report
|
||||||
from usage_monitor import fetch_and_render_usage_report
|
from usage_monitor import fetch_and_render_usage_report
|
||||||
from bot_actions import process_all_ai_actions
|
from bot_actions import process_all_ai_actions
|
||||||
@@ -326,6 +329,9 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🚀 انتشار به پروداکشن (Publish)", callback_data="btn_git_publish"),
|
InlineKeyboardButton("🚀 انتشار به پروداکشن (Publish)", callback_data="btn_git_publish"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("💻 ریموت SSH", callback_data="btn_ssh_menu"),
|
||||||
InlineKeyboardButton("🚀 دیپلوی FTP", callback_data="btn_ftp_menu"),
|
InlineKeyboardButton("🚀 دیپلوی FTP", callback_data="btn_ftp_menu"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -352,6 +358,9 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
InlineKeyboardButton("🚀 Publish to Production", callback_data="btn_git_publish"),
|
InlineKeyboardButton("🚀 Publish to Production", callback_data="btn_git_publish"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("💻 Remote SSH", callback_data="btn_ssh_menu"),
|
||||||
InlineKeyboardButton("🚀 Deploy via FTP", callback_data="btn_ftp_menu"),
|
InlineKeyboardButton("🚀 Deploy via FTP", callback_data="btn_ftp_menu"),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -454,6 +463,91 @@ async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
|||||||
return text, InlineKeyboardMarkup(keyboard)
|
return text, InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
async def build_ssh_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
|
||||||
|
"""Generates the interactive SSH Remote Dev and deployment view."""
|
||||||
|
session = session_manager.get_or_create(chat_id)
|
||||||
|
curr_proj = session_manager.get_current_project(chat_id)
|
||||||
|
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||||
|
|
||||||
|
if not curr_proj:
|
||||||
|
msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
|
||||||
|
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
|
||||||
|
|
||||||
|
text = format_ssh_info(
|
||||||
|
project_name=curr_proj.name,
|
||||||
|
host=curr_proj.ssh_host,
|
||||||
|
port=curr_proj.ssh_port,
|
||||||
|
user=curr_proj.ssh_user,
|
||||||
|
path=curr_proj.ssh_path,
|
||||||
|
password=curr_proj.ssh_password,
|
||||||
|
has_key=bool(curr_proj.ssh_key),
|
||||||
|
lang=session.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_fa:
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🚀 ارسال به پروداکشن (Push)", callback_data="btn_ssh_push"),
|
||||||
|
InlineKeyboardButton("📥 دریافت از سرور (Pull)", callback_data="btn_ssh_pull"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔍 تست اتصال SSH", callback_data="btn_ssh_test"),
|
||||||
|
InlineKeyboardButton("💻 اجرای فرمان ریموت", callback_data="btn_ssh_exec_prompt"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🌐 ویرایش هاست/پورت", callback_data="ssh_set:host"),
|
||||||
|
InlineKeyboardButton("👤 ویرایش نام کاربری", callback_data="ssh_set:user"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔑 ویرایش رمز عبور", callback_data="ssh_set:pass"),
|
||||||
|
InlineKeyboardButton("🗝️ تنظیم کلید SSH Key", callback_data="ssh_set:key"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("📂 ویرایش مسیر ریموت", callback_data="ssh_set:path"),
|
||||||
|
InlineKeyboardButton("📜 تاریخچه عملیات", callback_data="btn_ssh_logs"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🗑️ پاکسازی اطلاعات", callback_data="ssh_clear_conf"),
|
||||||
|
InlineKeyboardButton("🐙 منوی گیت و برنچها", callback_data="btn_git_menu"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
keyboard = [
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🚀 Push to Production", callback_data="btn_ssh_push"),
|
||||||
|
InlineKeyboardButton("📥 Pull from Server", callback_data="btn_ssh_pull"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔍 Test SSH Connection", callback_data="btn_ssh_test"),
|
||||||
|
InlineKeyboardButton("💻 Run Remote Command", callback_data="btn_ssh_exec_prompt"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🌐 Edit Host/Port", callback_data="ssh_set:host"),
|
||||||
|
InlineKeyboardButton("👤 Edit Username", callback_data="ssh_set:user"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔑 Edit Password", callback_data="ssh_set:pass"),
|
||||||
|
InlineKeyboardButton("🗝️ Set SSH Key", callback_data="ssh_set:key"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("📂 Edit Remote Path", callback_data="ssh_set:path"),
|
||||||
|
InlineKeyboardButton("📜 Audit Logs", callback_data="btn_ssh_logs"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🗑️ Clear Config", callback_data="ssh_clear_conf"),
|
||||||
|
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
return text, InlineKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]:
|
async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]:
|
||||||
"""Generates paginated commit history view."""
|
"""Generates paginated commit history view."""
|
||||||
@@ -3502,6 +3596,13 @@ async def ftp_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
text, markup = await build_ftp_menu(chat_id)
|
text, markup = await build_ftp_menu(chat_id)
|
||||||
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
|
# Command: /ssh or /remotedev
|
||||||
|
@check_auth
|
||||||
|
async def ssh_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
text, markup = await build_ssh_menu(chat_id)
|
||||||
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
# Command: /sync
|
# Command: /sync
|
||||||
@check_auth
|
@check_auth
|
||||||
async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
@@ -6723,6 +6824,108 @@ async def message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if state.get("type") == "ssh_set" and curr_proj:
|
||||||
|
field = state.get("field")
|
||||||
|
val = update.message.text.strip()
|
||||||
|
|
||||||
|
if field == "host":
|
||||||
|
if ":" in val:
|
||||||
|
h, p = val.rsplit(":", 1)
|
||||||
|
curr_proj.ssh_host = h.strip()
|
||||||
|
try:
|
||||||
|
curr_proj.ssh_port = int(p.strip())
|
||||||
|
except Exception:
|
||||||
|
curr_proj.ssh_port = 22
|
||||||
|
else:
|
||||||
|
curr_proj.ssh_host = val
|
||||||
|
curr_proj.ssh_port = 22
|
||||||
|
elif field == "user":
|
||||||
|
curr_proj.ssh_user = val
|
||||||
|
elif field == "pass":
|
||||||
|
curr_proj.ssh_password = val
|
||||||
|
try:
|
||||||
|
await update.message.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif field == "key":
|
||||||
|
curr_proj.ssh_key = val
|
||||||
|
try:
|
||||||
|
await update.message.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif field == "path":
|
||||||
|
curr_proj.ssh_path = val if val.startswith("/") else f"/{val}"
|
||||||
|
|
||||||
|
session_manager.save()
|
||||||
|
|
||||||
|
field_labels = {
|
||||||
|
"host": "هاست و پورت SSH",
|
||||||
|
"user": "نام کاربری SSH",
|
||||||
|
"pass": "رمز عبور SSH",
|
||||||
|
"key": "کلید اختصاصی SSH Key",
|
||||||
|
"path": "مسیر مقصد ریموت",
|
||||||
|
}
|
||||||
|
label_fa = field_labels.get(field, field)
|
||||||
|
ack_msg = f"✅ <b>{label_fa} با موفقیت ذخیره شد.</b>" if is_fa else f"✅ <b>SSH {field} updated successfully.</b>"
|
||||||
|
await update.message.reply_html(ack_msg)
|
||||||
|
|
||||||
|
# Auto-test connection if host is present
|
||||||
|
if curr_proj.ssh_host:
|
||||||
|
test_ok, test_msg = await ssh_manager.test_connection(
|
||||||
|
host=curr_proj.ssh_host,
|
||||||
|
port=curr_proj.ssh_port,
|
||||||
|
user=curr_proj.ssh_user or "",
|
||||||
|
password=curr_proj.ssh_password,
|
||||||
|
key=curr_proj.ssh_key,
|
||||||
|
path=curr_proj.ssh_path or "/",
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=curr_proj.name,
|
||||||
|
requester="user_button",
|
||||||
|
)
|
||||||
|
test_status = "✅ اتصال SSH با موفقیت بررسی و تایید شد." if test_ok else f"⚠️ نتیجه تست اتصال SSH: {test_msg}"
|
||||||
|
await update.message.reply_html(f"<i>{test_status}</i>")
|
||||||
|
|
||||||
|
text, markup = await build_ssh_menu(chat_id)
|
||||||
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if state.get("type") == "ssh_exec" and curr_proj:
|
||||||
|
cmd = update.message.text.strip()
|
||||||
|
if not curr_proj.ssh_host:
|
||||||
|
await update.message.reply_html("⚠️ اطلاعات سرور SSH تنظیم نشده است.")
|
||||||
|
text, markup = await build_ssh_menu(chat_id)
|
||||||
|
await update.message.reply_html(text, reply_markup=markup)
|
||||||
|
return
|
||||||
|
|
||||||
|
wait_msg = await update.message.reply_html(f"⏳ <i>در حال اجرای فرمان در سرور ریموت...</i>\n<code>{escape_html(cmd)}</code>")
|
||||||
|
ok, out, code = await ssh_manager.execute_command(
|
||||||
|
host=curr_proj.ssh_host,
|
||||||
|
port=curr_proj.ssh_port,
|
||||||
|
user=curr_proj.ssh_user or "",
|
||||||
|
password=curr_proj.ssh_password,
|
||||||
|
key=curr_proj.ssh_key,
|
||||||
|
remote_path=curr_proj.ssh_path or "/",
|
||||||
|
command=cmd,
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=curr_proj.name,
|
||||||
|
requester="user_command",
|
||||||
|
)
|
||||||
|
st_icon = "✅" if ok else "❌"
|
||||||
|
res_text = (
|
||||||
|
f"{st_icon} <b>نتیجه اجرای دستور ریموت:</b>\n"
|
||||||
|
f"• 💻 <b>دستور:</b> <code>{escape_html(cmd)}</code>\n"
|
||||||
|
f"• 🔢 <b>کد خروج:</b> <code>{code}</code>\n"
|
||||||
|
f"• 📜 <b>خروجی:</b>\n<pre>{escape_html(out)}</pre>"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await wait_msg.edit_text(res_text, parse_mode=constants.ParseMode.HTML)
|
||||||
|
except Exception:
|
||||||
|
await update.message.reply_html(res_text)
|
||||||
|
|
||||||
|
text, markup = await build_ssh_menu(chat_id)
|
||||||
|
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
|
||||||
|
return
|
||||||
|
|
||||||
if not curr_proj:
|
if not curr_proj:
|
||||||
if is_fa:
|
if is_fa:
|
||||||
msg = (
|
msg = (
|
||||||
|
|||||||
@@ -1623,7 +1623,301 @@ async def execute_action(
|
|||||||
return badge, side_effects, created_task
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# 35. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
|
# 35. SSH_CONFIG / SETUP_SSH / SET_SSH
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_CONFIG", "SETUP_SSH", "SET_SSH", "CONFIGURE_SSH"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
if not proj:
|
||||||
|
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||||
|
|
||||||
|
host = attrs.get("host") or attrs.get("server") or attrs.get("ssh_host")
|
||||||
|
port_raw = attrs.get("port") or attrs.get("ssh_port") or "22"
|
||||||
|
user = attrs.get("user") or attrs.get("username") or attrs.get("ssh_user") or ""
|
||||||
|
path = attrs.get("path") or attrs.get("dir") or attrs.get("remote_path") or attrs.get("ssh_path") or "/"
|
||||||
|
|
||||||
|
try:
|
||||||
|
port = int(port_raw)
|
||||||
|
except Exception:
|
||||||
|
port = 22
|
||||||
|
|
||||||
|
if host:
|
||||||
|
proj.ssh_host = host.strip()
|
||||||
|
proj.ssh_port = port
|
||||||
|
proj.ssh_user = user.strip() if user else proj.ssh_user
|
||||||
|
proj.ssh_path = path.strip()
|
||||||
|
session_manager.save()
|
||||||
|
|
||||||
|
from remote_audit import remote_audit
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="update_config",
|
||||||
|
requester="ai_agent",
|
||||||
|
status="success",
|
||||||
|
details=f"Host: {proj.ssh_host}:{proj.ssh_port}, Path: {proj.ssh_path}",
|
||||||
|
result_summary="SSH configuration updated",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
test_ok, test_msg = await ssh_manager.test_connection(
|
||||||
|
host=proj.ssh_host,
|
||||||
|
port=proj.ssh_port,
|
||||||
|
user=proj.ssh_user or "",
|
||||||
|
password=proj.ssh_password,
|
||||||
|
key=proj.ssh_key,
|
||||||
|
path=proj.ssh_path,
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
requester="ai_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
status_icon = "✅" if test_ok else "⚠️"
|
||||||
|
badge = (
|
||||||
|
f"\n\n💻 <b>تنظیمات اتصال SSH برای پروژه <code>{escape_html(proj.name)}</code> بهروز شد:</b>\n"
|
||||||
|
f"• 🌐 <b>هاست:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 👤 <b>نام کاربری:</b> <code>{escape_html(proj.ssh_user or '(پیشفرض root)')}</code>\n"
|
||||||
|
f"• 📂 <b>مسیر ریموت:</b> <code>{escape_html(proj.ssh_path)}</code>\n"
|
||||||
|
f"• {status_icon} <b>نتیجه تست اتصال:</b> {test_msg}"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\n💻 <b>SSH Configuration updated for project <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 🌐 <b>Host:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 👤 <b>User:</b> <code>{escape_html(proj.ssh_user or '(default root)')}</code>\n"
|
||||||
|
f"• 📂 <b>Remote Path:</b> <code>{escape_html(proj.ssh_path)}</code>\n"
|
||||||
|
f"• {status_icon} <b>Connection test:</b> {test_msg}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
curr_host = proj.ssh_host or "(تنظیم نشده)"
|
||||||
|
badge = (
|
||||||
|
f"\n\nℹ️ <b>وضعیت تنظیمات SSH پروژه <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 🌐 <b>هاست:</b> <code>{escape_html(curr_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 👤 <b>کاربر:</b> <code>{escape_html(proj.ssh_user or '(تعیین نشده)')}</code>\n"
|
||||||
|
f"• 📂 <b>مسیر:</b> <code>{escape_html(proj.ssh_path or '/')}</code>"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\nℹ️ <b>SSH Status for <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 👤 <b>User:</b> <code>{escape_html(proj.ssh_user or '(none)')}</code>\n"
|
||||||
|
f"• 📂 <b>Path:</b> <code>{escape_html(proj.ssh_path or '/')}</code>"
|
||||||
|
)
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 36. SSH_TEST / TEST_SSH
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_TEST", "TEST_SSH", "CHECK_SSH"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
if not proj:
|
||||||
|
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||||
|
|
||||||
|
if not proj.ssh_host:
|
||||||
|
return "\n⚠️ اطلاعات سرور SSH برای این پروژه ثبت نشده است. میتوانید از منوی <code>/ssh</code> آن را تنظیم کنید." if is_fa else "\n⚠️ SSH credentials are not set for this project.", side_effects, created_task
|
||||||
|
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
test_ok, test_msg = await ssh_manager.test_connection(
|
||||||
|
host=proj.ssh_host,
|
||||||
|
port=proj.ssh_port,
|
||||||
|
user=proj.ssh_user or "",
|
||||||
|
password=proj.ssh_password,
|
||||||
|
key=proj.ssh_key,
|
||||||
|
path=proj.ssh_path or "/",
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
requester="ai_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
icon = "✅" if test_ok else "❌"
|
||||||
|
badge = (
|
||||||
|
f"\n\n{icon} <b>تست اتصال سرور SSH پروژه <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 🌐 <b>سرور:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 📂 <b>مسیر:</b> <code>{escape_html(proj.ssh_path or '/')}</code>\n"
|
||||||
|
f"• 📋 <b>وضعیت:</b>\n{test_msg}"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\n{icon} <b>SSH Test Result for <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 🌐 <b>Server:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code>\n"
|
||||||
|
f"• 📂 <b>Path:</b> <code>{escape_html(proj.ssh_path or '/')}</code>\n"
|
||||||
|
f"• 📋 <b>Status:</b>\n{test_msg}"
|
||||||
|
)
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 37. SSH_PUSH / PUSH_SSH / SSH_DEPLOY
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_PUSH", "PUSH_SSH", "SSH_DEPLOY", "DEPLOY_SSH"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
if not proj:
|
||||||
|
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||||
|
|
||||||
|
if not proj.ssh_host:
|
||||||
|
return "\n⚠️ مشخصات سرور SSH برای این پروژه یافت نشد. لطفاً ابتدا در منوی <code>/ssh</code> مشخصات را تنظیم نمایید." if is_fa else "\n⚠️ SSH configuration not found for this project.", side_effects, created_task
|
||||||
|
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
from git_manager import git_manager
|
||||||
|
try:
|
||||||
|
await git_manager.git_publish(proj.workspace, message="Auto-publish before SSH push", repo_name=proj.name)
|
||||||
|
except Exception as pe:
|
||||||
|
logger.warning(f"Auto-publish warning before SSH push: {pe}")
|
||||||
|
|
||||||
|
res = await ssh_manager.push_project(
|
||||||
|
workspace_path=proj.workspace,
|
||||||
|
host=proj.ssh_host,
|
||||||
|
port=proj.ssh_port,
|
||||||
|
user=proj.ssh_user or "",
|
||||||
|
password=proj.ssh_password,
|
||||||
|
key=proj.ssh_key,
|
||||||
|
remote_path=proj.ssh_path or "/",
|
||||||
|
branch="production",
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
requester="ai_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.get("success"):
|
||||||
|
kb = round(res.get("bytes_transferred", 0) / 1024, 2)
|
||||||
|
mb = round(kb / 1024, 2)
|
||||||
|
size_str = f"{mb} مگابایت" if mb >= 1.0 else f"{kb} کیلوبایت"
|
||||||
|
files_up = res.get("files_uploaded", 0)
|
||||||
|
files_skip = res.get("files_skipped", 0)
|
||||||
|
duration = round(res.get("duration", 0), 2)
|
||||||
|
target_p = proj.ssh_path or "/"
|
||||||
|
|
||||||
|
badge = (
|
||||||
|
f"\n\n🚀 <b>کدهای شاخه production با موفقیت به سرور SSH منتقل شدند:</b>\n"
|
||||||
|
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||||
|
f"• 🌐 <b>مقصد ریموت:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code> (مسیر: <code>{escape_html(target_p)}</code>)\n"
|
||||||
|
f"• 📤 <b>فایلهای آپلودشده:</b> <b>{files_up}</b> فایل ({size_str})\n"
|
||||||
|
f"• 🛡️ <b>دادههای محلی/دیتابیس/آپلود نادیده گرفتهشده:</b> <b>{files_skip}</b> آیتم\n"
|
||||||
|
f"• 🔒 <b>دادههای سرور پروداکشن:</b> کاملاً دستنخورده و ایمن حفظ شدند\n"
|
||||||
|
f"• ⏱️ <b>مدت زمان انتقال:</b> <code>{duration}</code> ثانیه"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\n🚀 <b>Production branch pushed successfully to SSH server:</b>\n"
|
||||||
|
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||||
|
f"• 🌐 <b>Remote Target:</b> <code>{escape_html(proj.ssh_host)}:{proj.ssh_port}</code> (Path: <code>{escape_html(target_p)}</code>)\n"
|
||||||
|
f"• 📤 <b>Files Uploaded:</b> <b>{files_up}</b> ({size_str})\n"
|
||||||
|
f"• 🛡️ <b>Skipped Junk/Databases:</b> <b>{files_skip}</b> items\n"
|
||||||
|
f"• 🔒 <b>Remote production data:</b> Intact & Protected\n"
|
||||||
|
f"• ⏱️ <b>Duration:</b> <code>{duration}s</code>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err = res.get("error", "Unknown error")
|
||||||
|
badge = f"\n⚠️ <b>خطا در پوش SSH به سرور مقصد:</b> {escape_html(str(err))}" if is_fa else f"\n⚠️ <b>SSH Push Failed:</b> {escape_html(str(err))}"
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 38. SSH_PULL / PULL_SSH / SSH_FETCH
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_PULL", "PULL_SSH", "SSH_FETCH", "FETCH_SSH"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
if not proj:
|
||||||
|
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||||
|
|
||||||
|
if not proj.ssh_host:
|
||||||
|
return "\n⚠️ مشخصات سرور SSH برای این پروژه یافت نشد." if is_fa else "\n⚠️ SSH configuration not found for this project.", side_effects, created_task
|
||||||
|
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
res = await ssh_manager.pull_project(
|
||||||
|
workspace_path=proj.workspace,
|
||||||
|
host=proj.ssh_host,
|
||||||
|
port=proj.ssh_port,
|
||||||
|
user=proj.ssh_user or "",
|
||||||
|
password=proj.ssh_password,
|
||||||
|
key=proj.ssh_key,
|
||||||
|
remote_path=proj.ssh_path or "/",
|
||||||
|
branch="production",
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
requester="ai_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
if res.get("success"):
|
||||||
|
kb = round(res.get("bytes_transferred", 0) / 1024, 2)
|
||||||
|
mb = round(kb / 1024, 2)
|
||||||
|
size_str = f"{mb} مگابایت" if mb >= 1.0 else f"{kb} کیلوبایت"
|
||||||
|
files_down = res.get("files_downloaded", 0)
|
||||||
|
files_skip = res.get("files_skipped", 0)
|
||||||
|
duration = round(res.get("duration", 0), 2)
|
||||||
|
|
||||||
|
badge = (
|
||||||
|
f"\n\n📥 <b>فایلهای پروژه از سرور SSH با موفقیت دریافت شدند (Pull):</b>\n"
|
||||||
|
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||||
|
f"• 📥 <b>فایلهای دریافتشده:</b> <b>{files_down}</b> فایل ({size_str})\n"
|
||||||
|
f"• 🛡️ <b>فایلهای نادیده گرفتهشده (دیتابیس/لاگ/آپلود):</b> <b>{files_skip}</b> آیتم\n"
|
||||||
|
f"• ⏱️ <b>مدت زمان انتقال:</b> <code>{duration}</code> ثانیه"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\n📥 <b>Project pulled successfully from SSH server:</b>\n"
|
||||||
|
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
|
||||||
|
f"• 📥 <b>Files Downloaded:</b> <b>{files_down}</b> ({size_str})\n"
|
||||||
|
f"• 🛡️ <b>Skipped (Databases/Logs/Uploads):</b> <b>{files_skip}</b> items\n"
|
||||||
|
f"• ⏱️ <b>Duration:</b> <code>{duration}s</code>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
err = res.get("error", "Unknown error")
|
||||||
|
badge = f"\n⚠️ <b>خطا در دریافت فایلها از سرور SSH:</b> {escape_html(str(err))}" if is_fa else f"\n⚠️ <b>SSH Pull Failed:</b> {escape_html(str(err))}"
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 39. SSH_EXEC / EXEC_SSH / RUN_SSH
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_EXEC", "EXEC_SSH", "RUN_SSH", "SSH_COMMAND"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
if not proj:
|
||||||
|
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
|
||||||
|
|
||||||
|
if not proj.ssh_host:
|
||||||
|
return "\n⚠️ مشخصات سرور SSH برای این پروژه یافت نشد." if is_fa else "\n⚠️ SSH configuration not found for this project.", side_effects, created_task
|
||||||
|
|
||||||
|
cmd = attrs.get("command") or attrs.get("cmd") or attrs.get("_default") or ""
|
||||||
|
if not cmd.strip():
|
||||||
|
return "\n⚠️ دستور اجرایی برای سرور SSH تعیین نشده است." if is_fa else "\n⚠️ SSH command is required.", side_effects, created_task
|
||||||
|
|
||||||
|
from ssh_manager import ssh_manager
|
||||||
|
ok, out, code = await ssh_manager.execute_command(
|
||||||
|
host=proj.ssh_host,
|
||||||
|
port=proj.ssh_port,
|
||||||
|
user=proj.ssh_user or "",
|
||||||
|
password=proj.ssh_password,
|
||||||
|
key=proj.ssh_key,
|
||||||
|
remote_path=proj.ssh_path or "/",
|
||||||
|
command=cmd,
|
||||||
|
user_id=chat_id,
|
||||||
|
project_name=proj.name,
|
||||||
|
requester="ai_agent",
|
||||||
|
)
|
||||||
|
|
||||||
|
st_icon = "✅" if ok else "❌"
|
||||||
|
badge = (
|
||||||
|
f"\n\n{st_icon} <b>اجرای دستور روی سرور SSH پروژه <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 💻 <b>دستور:</b> <code>{escape_html(cmd)}</code>\n"
|
||||||
|
f"• 🔢 <b>کد خروج:</b> <code>{code}</code>\n"
|
||||||
|
f"• 📜 <b>خروجی سرور:</b>\n<pre>{escape_html(out)}</pre>"
|
||||||
|
if is_fa else
|
||||||
|
f"\n\n{st_icon} <b>SSH Command Execution on <code>{escape_html(proj.name)}</code>:</b>\n"
|
||||||
|
f"• 💻 <b>Command:</b> <code>{escape_html(cmd)}</code>\n"
|
||||||
|
f"• 🔢 <b>Exit Code:</b> <code>{code}</code>\n"
|
||||||
|
f"• 📜 <b>Output:</b>\n<pre>{escape_html(out)}</pre>"
|
||||||
|
)
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 40. SSH_LOGS / REMOTE_LOGS / AUDIT_LOGS
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
elif act in ("SSH_LOGS", "REMOTE_LOGS", "AUDIT_LOGS", "SSH_AUDIT"):
|
||||||
|
p_name = attrs.get("project") or attrs.get("name") or current_project_name
|
||||||
|
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
|
||||||
|
proj_name = proj.name if proj else (p_name or "default")
|
||||||
|
|
||||||
|
from remote_audit import remote_audit
|
||||||
|
logs = remote_audit.get_recent_logs(user_id=chat_id, project_name=proj_name, limit=10)
|
||||||
|
badge = "\n\n" + remote_audit.format_logs_for_tg(logs, project_name=proj_name, is_fa=is_fa)
|
||||||
|
return badge, side_effects, created_task
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 41. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
|
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
|
||||||
from memory_manager import memory_manager
|
from memory_manager import memory_manager
|
||||||
|
|||||||
@@ -659,4 +659,62 @@ def format_ftp_info(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def format_ssh_info(
|
||||||
|
project_name: str,
|
||||||
|
host: Optional[str],
|
||||||
|
port: int = 22,
|
||||||
|
user: Optional[str] = None,
|
||||||
|
path: str = "/",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
has_key: bool = False,
|
||||||
|
lang: str = "fa",
|
||||||
|
) -> str:
|
||||||
|
"""Formats SSH connection and remote development status for Telegram HTML."""
|
||||||
|
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
|
||||||
|
clean_p = escape_html(project_name)
|
||||||
|
host_str = f"<code>{escape_html(host)}:{port}</code>" if host else ("<i>(تنظیم نشده)</i>" if is_fa else "<i>(Not configured)</i>")
|
||||||
|
user_str = f"<code>{escape_html(user)}</code>" if user else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
|
||||||
|
path_str = f"<code>{escape_html(path)}</code>"
|
||||||
|
|
||||||
|
auth_methods = []
|
||||||
|
if password:
|
||||||
|
auth_methods.append("رمز عبور 🔑" if is_fa else "Password 🔑")
|
||||||
|
if has_key:
|
||||||
|
auth_methods.append("کلید اختصاصی SSH 🗝️" if is_fa else "SSH Private Key 🗝️")
|
||||||
|
if not auth_methods:
|
||||||
|
auth_str = "<i>(تعیین نشده)</i>" if is_fa else "<i>(Not set)</i>"
|
||||||
|
else:
|
||||||
|
auth_str = " + ".join(auth_methods)
|
||||||
|
|
||||||
|
if is_fa:
|
||||||
|
return (
|
||||||
|
f"💻 <b>تنظیمات ریموت دولوپ و سرور SSH پروژه</b>\n\n"
|
||||||
|
f"• 📁 <b>پروژه:</b> <code>{clean_p}</code>\n"
|
||||||
|
f"• 🌐 <b>سرور / هاست:</b> {host_str}\n"
|
||||||
|
f"• 👤 <b>نام کاربری:</b> {user_str}\n"
|
||||||
|
f"• 🔐 <b>نوع احراز هویت:</b> {auth_str}\n"
|
||||||
|
f"• 📂 <b>مسیر مقصد روی سرور:</b> {path_str}\n\n"
|
||||||
|
f"🛡️ <b>اصول ایمنی پروداکشن:</b>\n"
|
||||||
|
f"├ پوش کدهای محلی منحصراً از شاخه <code>production</code> انجام میشود.\n"
|
||||||
|
f"├ فایلهای دیتابیس (SQLite)، آپلودهای کاربران و فایلهای سشن سرور ریموت دستنخورده باقی میمانند.\n"
|
||||||
|
f"└ تمام درخواستها و لاگها به صورت خودکار در دیتابیس ثبت میشوند.\n\n"
|
||||||
|
f"💡 <i>از دکمههای زیر برای تست اتصال، ارسال کدها (Push)، دریافت کدها (Pull) یا مدیریت مشخصات استفاده کنید.</i>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
f"💻 <b>Project SSH Remote Dev & Server Settings</b>\n\n"
|
||||||
|
f"• 📁 <b>Project:</b> <code>{clean_p}</code>\n"
|
||||||
|
f"• 🌐 <b>SSH Host:</b> {host_str}\n"
|
||||||
|
f"• 👤 <b>Username:</b> {user_str}\n"
|
||||||
|
f"• 🔐 <b>Authentication:</b> {auth_str}\n"
|
||||||
|
f"• 📂 <b>Remote Path:</b> {path_str}\n\n"
|
||||||
|
f"🛡️ <b>Production Safety:</b>\n"
|
||||||
|
f"├ Pushes strictly deploy from <code>production</code> branch.\n"
|
||||||
|
f"├ Remote user uploads, SQLite databases, and session files are preserved.\n"
|
||||||
|
f"└ All operations are audited in SQLite.\n\n"
|
||||||
|
f"💡 <i>Use buttons below to test connection, push, pull, or manage server config.</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger("AGYRemoteAudit")
|
||||||
|
|
||||||
|
DATA_DIR = Path("/root/telegram-agy-bot/data")
|
||||||
|
DB_PATH = DATA_DIR / "memory.db"
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteAuditLogger:
|
||||||
|
"""Manages audit logging for SSH, FTP, and remote operations into SQLite."""
|
||||||
|
|
||||||
|
def __init__(self, db_path: Path = DB_PATH):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
|
def _get_connection(self) -> sqlite3.Connection:
|
||||||
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = sqlite3.connect(str(self.db_path), timeout=15.0)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
conn.execute("PRAGMA synchronous=NORMAL;")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _init_db(self):
|
||||||
|
"""Initializes remote_audit_logs table and indexes."""
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS remote_audit_logs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp REAL NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
project_name TEXT NOT NULL,
|
||||||
|
service_type TEXT NOT NULL, -- 'ssh', 'ftp'
|
||||||
|
action_type TEXT NOT NULL, -- 'push', 'pull', 'exec', 'test_connection', 'update_config'
|
||||||
|
requester TEXT NOT NULL, -- 'ai_agent', 'user_button', 'user_command'
|
||||||
|
details TEXT, -- command or parameters (never contains passwords)
|
||||||
|
status TEXT NOT NULL, -- 'success', 'failed', 'running'
|
||||||
|
result_summary TEXT, -- short summary or error message
|
||||||
|
duration_ms REAL DEFAULT 0.0
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_remote_audit_lookup
|
||||||
|
ON remote_audit_logs(user_id, project_name, service_type, timestamp DESC);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing remote_audit_logs table: {e}")
|
||||||
|
|
||||||
|
def log_operation(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
project_name: str,
|
||||||
|
service_type: str,
|
||||||
|
action_type: str,
|
||||||
|
requester: str,
|
||||||
|
status: str,
|
||||||
|
details: Optional[str] = None,
|
||||||
|
result_summary: Optional[str] = None,
|
||||||
|
duration_ms: float = 0.0,
|
||||||
|
timestamp: Optional[float] = None,
|
||||||
|
) -> int:
|
||||||
|
"""Logs a remote operation into the audit database."""
|
||||||
|
now_ts = timestamp or time.time()
|
||||||
|
dt_str = datetime.fromtimestamp(now_ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Sanitize details (ensure no raw passwords accidentally entered)
|
||||||
|
safe_details = details or ""
|
||||||
|
if "password" in safe_details.lower():
|
||||||
|
safe_details = "[FILTERED DETAILS]"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cur = conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO remote_audit_logs (
|
||||||
|
timestamp, created_at, user_id, project_name,
|
||||||
|
service_type, action_type, requester, details,
|
||||||
|
status, result_summary, duration_ms
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
now_ts,
|
||||||
|
dt_str,
|
||||||
|
int(user_id),
|
||||||
|
str(project_name),
|
||||||
|
str(service_type).lower(),
|
||||||
|
str(action_type).lower(),
|
||||||
|
str(requester).lower(),
|
||||||
|
safe_details[:1000] if safe_details else None,
|
||||||
|
str(status).lower(),
|
||||||
|
str(result_summary)[:1000] if result_summary else None,
|
||||||
|
float(duration_ms),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return cur.lastrowid
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error writing to remote_audit_logs: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_recent_logs(
|
||||||
|
self,
|
||||||
|
user_id: Optional[int] = None,
|
||||||
|
project_name: Optional[str] = None,
|
||||||
|
service_type: Optional[str] = None,
|
||||||
|
limit: int = 15,
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Retrieves recent audit log records."""
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
query = "SELECT * FROM remote_audit_logs WHERE 1=1"
|
||||||
|
params: List[Any] = []
|
||||||
|
if user_id is not None:
|
||||||
|
query += " AND user_id = ?"
|
||||||
|
params.append(user_id)
|
||||||
|
if project_name:
|
||||||
|
query += " AND project_name = ?"
|
||||||
|
params.append(project_name)
|
||||||
|
if service_type:
|
||||||
|
query += " AND service_type = ?"
|
||||||
|
params.append(service_type)
|
||||||
|
|
||||||
|
query += " ORDER BY timestamp DESC LIMIT ?"
|
||||||
|
params.append(int(limit))
|
||||||
|
|
||||||
|
cur = conn.execute(query, params)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching remote_audit_logs: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def format_logs_for_tg(
|
||||||
|
self,
|
||||||
|
logs: List[Dict[str, Any]],
|
||||||
|
project_name: str,
|
||||||
|
is_fa: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""Formats audit logs into a clean Telegram HTML message."""
|
||||||
|
if not logs:
|
||||||
|
if is_fa:
|
||||||
|
return (
|
||||||
|
f"📜 <b>تاریخچه و لاگ عملیات ریموت</b>\n\n"
|
||||||
|
f"• 📁 <b>پروژه:</b> <code>{project_name}</code>\n\n"
|
||||||
|
f"ℹ️ <i>هنوز هیچ عملیات SSH یا FTP برای این پروژه ثبت نشده است.</i>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
f"📜 <b>Remote Operations Audit Log</b>\n\n"
|
||||||
|
f"• 📁 <b>Project:</b> <code>{project_name}</code>\n\n"
|
||||||
|
f"ℹ️ <i>No SSH or FTP operations logged for this project yet.</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if is_fa:
|
||||||
|
lines.append(f"📜 <b>تاریخچه عملیات ریموت و SSH/FTP</b>")
|
||||||
|
lines.append(f"• 📁 <b>پروژه:</b> <code>{project_name}</code>")
|
||||||
|
lines.append(f"• 📊 <b>تعداد لاگهای اخیر:</b> <code>{len(logs)}</code>\n")
|
||||||
|
else:
|
||||||
|
lines.append(f"📜 <b>Remote Operations & SSH/FTP Audit Log</b>")
|
||||||
|
lines.append(f"• 📁 <b>Project:</b> <code>{project_name}</code>")
|
||||||
|
lines.append(f"• 📊 <b>Recent records:</b> <code>{len(logs)}</code>\n")
|
||||||
|
|
||||||
|
for idx, log in enumerate(logs, 1):
|
||||||
|
st = log.get("status", "unknown")
|
||||||
|
st_icon = "✅" if st == "success" else ("❌" if st == "failed" else "⏳")
|
||||||
|
srv = log.get("service_type", "").upper()
|
||||||
|
act = log.get("action_type", "")
|
||||||
|
req = log.get("requester", "")
|
||||||
|
req_label = "🤖 AI" if "ai" in req else ("👤 کاربر" if is_fa else "👤 User")
|
||||||
|
dt = log.get("created_at", "")
|
||||||
|
dur = log.get("duration_ms", 0.0)
|
||||||
|
dur_str = f" ({dur:.1f}ms)" if dur > 0 else ""
|
||||||
|
res = log.get("result_summary") or ""
|
||||||
|
det = log.get("details") or ""
|
||||||
|
|
||||||
|
act_fa = {
|
||||||
|
"push": "پوش به پروداکشن 🚀",
|
||||||
|
"pull": "دریافت از سرور 📥",
|
||||||
|
"exec": "اجرای فرمان 💻",
|
||||||
|
"test_connection": "تست اتصال 🔍",
|
||||||
|
"update_config": "ویرایش تنظیمات ⚙️",
|
||||||
|
}.get(act, act)
|
||||||
|
|
||||||
|
act_display = act_fa if is_fa else act.capitalize()
|
||||||
|
|
||||||
|
item_text = f"<b>{idx}. {st_icon} [{srv}] {act_display}</b> | {req_label}\n"
|
||||||
|
item_text += f" 🕒 <code>{dt}</code>{dur_str}\n"
|
||||||
|
if det:
|
||||||
|
item_text += f" 📝 <code>{det[:60]}</code>\n"
|
||||||
|
if res:
|
||||||
|
item_text += f" 💬 <i>{res[:100]}</i>\n"
|
||||||
|
|
||||||
|
lines.append(item_text)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# Global singleton instance
|
||||||
|
remote_audit = RemoteAuditLogger()
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,719 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import fnmatch
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List, Tuple, Set
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import io
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
from remote_audit import remote_audit
|
||||||
|
|
||||||
|
logger = logging.getLogger("AGYSSHManager")
|
||||||
|
|
||||||
|
# Standard patterns for junk, temporary, local configs, and production user data that must NEVER be overwritten or pushed
|
||||||
|
DEFAULT_SSH_EXCLUDES = [
|
||||||
|
# Git and version control
|
||||||
|
".git",
|
||||||
|
".git/*",
|
||||||
|
".gitignore",
|
||||||
|
".gitattributes",
|
||||||
|
".gitmodules",
|
||||||
|
|
||||||
|
# Environment & local secret files
|
||||||
|
".env",
|
||||||
|
".env.*",
|
||||||
|
"*.env",
|
||||||
|
"*.pem",
|
||||||
|
"*.key",
|
||||||
|
|
||||||
|
# User databases and data stores (never overwrite production sqlite / db files!)
|
||||||
|
"*.db",
|
||||||
|
"*.db-shm",
|
||||||
|
"*.db-wal",
|
||||||
|
"*.sqlite",
|
||||||
|
"*.sqlite3",
|
||||||
|
"data/*.db",
|
||||||
|
"data/*.sqlite",
|
||||||
|
|
||||||
|
# Dynamic uploads & media directories (production user uploads must never be touched)
|
||||||
|
"uploads",
|
||||||
|
"uploads/*",
|
||||||
|
"*/uploads",
|
||||||
|
"*/uploads/*",
|
||||||
|
"media",
|
||||||
|
"media/*",
|
||||||
|
"storage/uploads",
|
||||||
|
"storage/uploads/*",
|
||||||
|
"storage/framework",
|
||||||
|
"storage/framework/*",
|
||||||
|
|
||||||
|
# AI & Bot internal session / agent configs
|
||||||
|
".agents",
|
||||||
|
".agents/*",
|
||||||
|
".gemini",
|
||||||
|
".gemini/*",
|
||||||
|
"sessions_data",
|
||||||
|
"sessions_data/*",
|
||||||
|
"uploads_temp",
|
||||||
|
"uploads_temp/*",
|
||||||
|
|
||||||
|
# Python cache & virtual environments
|
||||||
|
"__pycache__",
|
||||||
|
"__pycache__/*",
|
||||||
|
"*.pyc",
|
||||||
|
"*.pyo",
|
||||||
|
"*.pyd",
|
||||||
|
".venv",
|
||||||
|
".venv/*",
|
||||||
|
"venv",
|
||||||
|
"venv/*",
|
||||||
|
"env",
|
||||||
|
"env/*",
|
||||||
|
|
||||||
|
# Node.js dependencies
|
||||||
|
"node_modules",
|
||||||
|
"node_modules/*",
|
||||||
|
|
||||||
|
# Logs & temporary files
|
||||||
|
"*.log",
|
||||||
|
"logs",
|
||||||
|
"logs/*",
|
||||||
|
"tmp",
|
||||||
|
"tmp/*",
|
||||||
|
"temp",
|
||||||
|
"temp/*",
|
||||||
|
|
||||||
|
# IDE & OS files
|
||||||
|
".DS_Store",
|
||||||
|
"Thumbs.db",
|
||||||
|
"desktop.ini",
|
||||||
|
".idea",
|
||||||
|
".idea/*",
|
||||||
|
".vscode",
|
||||||
|
".vscode/*",
|
||||||
|
"*.swp",
|
||||||
|
"*.swo",
|
||||||
|
"*~",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def should_exclude_ssh(rel_path: str, custom_excludes: Optional[List[str]] = None) -> bool:
|
||||||
|
"""Checks if a relative path matches any exclusion pattern for SSH sync."""
|
||||||
|
excludes = DEFAULT_SSH_EXCLUDES + (custom_excludes or [])
|
||||||
|
norm_path = rel_path.replace("\\", "/").strip("/")
|
||||||
|
parts = [p for p in norm_path.split("/") if p]
|
||||||
|
|
||||||
|
for pattern in excludes:
|
||||||
|
pat = pattern.replace("\\", "/").strip("/")
|
||||||
|
clean_pat = pat.rstrip("/*")
|
||||||
|
|
||||||
|
if fnmatch.fnmatch(norm_path, pat) or fnmatch.fnmatch(norm_path, clean_pat):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if fnmatch.fnmatch(part, pat) or fnmatch.fnmatch(part, clean_pat):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SSHManager:
|
||||||
|
"""Manages SSH connections, remote dev synchronization, execution, and audit logging."""
|
||||||
|
|
||||||
|
def _create_ssh_client(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int = 22,
|
||||||
|
user: str = "",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
key_content_or_path: Optional[str] = None,
|
||||||
|
timeout: int = 15,
|
||||||
|
) -> paramiko.SSHClient:
|
||||||
|
"""Helper to create and authenticate a paramiko SSHClient."""
|
||||||
|
clean_host = host.strip()
|
||||||
|
clean_user = user.strip() if user else "root"
|
||||||
|
clean_pass = password.strip() if password else None
|
||||||
|
clean_port = int(port) if port else 22
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
pkey = None
|
||||||
|
if key_content_or_path and key_content_or_path.strip():
|
||||||
|
raw_k = key_content_or_path.strip()
|
||||||
|
# Check if key is a file path
|
||||||
|
if os.path.isfile(raw_k):
|
||||||
|
try:
|
||||||
|
pkey = paramiko.RSAKey.from_private_key_file(raw_k, password=clean_pass)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(raw_k, password=clean_pass)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
pkey = paramiko.ECDSAKey.from_private_key_file(raw_k, password=clean_pass)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not load private key from file {raw_k}: {e}")
|
||||||
|
else:
|
||||||
|
# Key is provided as string text
|
||||||
|
key_file_obj = io.StringIO(raw_k)
|
||||||
|
for key_cls in (paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey):
|
||||||
|
try:
|
||||||
|
key_file_obj.seek(0)
|
||||||
|
pkey = key_cls.from_private_key(key_file_obj, password=clean_pass)
|
||||||
|
if pkey:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
connect_kwargs: Dict[str, Any] = {
|
||||||
|
"hostname": clean_host,
|
||||||
|
"port": clean_port,
|
||||||
|
"username": clean_user,
|
||||||
|
"timeout": timeout,
|
||||||
|
"banner_timeout": timeout,
|
||||||
|
"auth_timeout": timeout,
|
||||||
|
"allow_agent": False,
|
||||||
|
"look_for_keys": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if pkey:
|
||||||
|
connect_kwargs["pkey"] = pkey
|
||||||
|
elif clean_pass:
|
||||||
|
connect_kwargs["password"] = clean_pass
|
||||||
|
else:
|
||||||
|
# Try agent / default keys if neither provided
|
||||||
|
connect_kwargs["allow_agent"] = True
|
||||||
|
connect_kwargs["look_for_keys"] = True
|
||||||
|
|
||||||
|
client.connect(**connect_kwargs)
|
||||||
|
return client
|
||||||
|
|
||||||
|
async def test_connection(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int = 22,
|
||||||
|
user: str = "",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
path: str = "/",
|
||||||
|
timeout: int = 12,
|
||||||
|
user_id: int = 0,
|
||||||
|
project_name: str = "",
|
||||||
|
requester: str = "user_button",
|
||||||
|
) -> Tuple[bool, str]:
|
||||||
|
"""Tests SSH connection and checks remote path accessibility."""
|
||||||
|
start_t = time.time()
|
||||||
|
if not host or not host.strip():
|
||||||
|
return False, "آدرس هاست (Host) سرور SSH مشخص نشده است."
|
||||||
|
|
||||||
|
def _test():
|
||||||
|
client = None
|
||||||
|
try:
|
||||||
|
client = self._create_ssh_client(
|
||||||
|
host=host.strip(),
|
||||||
|
port=port,
|
||||||
|
user=user.strip(),
|
||||||
|
password=password,
|
||||||
|
key_content_or_path=key,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run simple probe command
|
||||||
|
stdin, stdout, stderr = client.exec_command("uname -sr || echo Linux", timeout=timeout)
|
||||||
|
os_info = stdout.read().decode("utf-8", errors="ignore").strip()
|
||||||
|
|
||||||
|
# Test SFTP session and target path
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
target_path = (path or "/").strip()
|
||||||
|
path_status = "موجود است"
|
||||||
|
item_count = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
sftp.stat(target_path)
|
||||||
|
try:
|
||||||
|
dir_items = sftp.listdir(target_path)
|
||||||
|
item_count = len(dir_items)
|
||||||
|
except Exception:
|
||||||
|
item_count = 0
|
||||||
|
except IOError:
|
||||||
|
path_status = "موجود نیست (در اولین استقرار به صورت خودکار ایجاد میشود)"
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
msg = (
|
||||||
|
f"اتصال SSH با موفقیت برقرار شد ✅\n"
|
||||||
|
f"• 🖥️ <b>سیستمعامل:</b> <code>{os_info}</code>\n"
|
||||||
|
f"• 📂 <b>مسیر ریموت:</b> <code>{target_path}</code> ({path_status})\n"
|
||||||
|
f"• 📊 <b>تعداد آیتمها در مسیر:</b> <code>{item_count}</code>"
|
||||||
|
)
|
||||||
|
return True, msg
|
||||||
|
|
||||||
|
except socket.gaierror as e:
|
||||||
|
return False, f"نام دامنه یا IP سرور یافت نشد ({host}): {e}"
|
||||||
|
except (socket.timeout, TimeoutError) as e:
|
||||||
|
return False, f"مهلت زمانی اتصال SSH به پایان رسید (Timeout روی پورت {port}): {e}"
|
||||||
|
except ConnectionRefusedError as e:
|
||||||
|
return False, f"اتصال SSH توسط سرور رد شد (پورت {port} مسدود است یا SSH اجرا نمیشود)."
|
||||||
|
except paramiko.AuthenticationException as e:
|
||||||
|
return False, f"خطای احراز هویت SSH: نام کاربری، رمز عبور یا کلید نامعتبر است ({e})."
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"خطا در برقراری ارتباط SSH: {e}"
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
try:
|
||||||
|
client.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
success, result_msg = await asyncio.to_thread(_test)
|
||||||
|
dur = (time.time() - start_t) * 1000
|
||||||
|
|
||||||
|
# Record audit log
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project_name or "unknown",
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="test_connection",
|
||||||
|
requester=requester,
|
||||||
|
status="success" if success else "failed",
|
||||||
|
details=f"Host: {host}:{port}, Path: {path}",
|
||||||
|
result_summary=result_msg[:200],
|
||||||
|
duration_ms=dur,
|
||||||
|
)
|
||||||
|
|
||||||
|
return success, result_msg
|
||||||
|
|
||||||
|
async def push_project(
|
||||||
|
self,
|
||||||
|
workspace_path: str,
|
||||||
|
host: str,
|
||||||
|
port: int = 22,
|
||||||
|
user: str = "",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
remote_path: str = "/",
|
||||||
|
branch: str = "production",
|
||||||
|
custom_excludes: Optional[List[str]] = None,
|
||||||
|
user_id: int = 0,
|
||||||
|
project_name: str = "",
|
||||||
|
requester: str = "user_button",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Pushes files from the git branch (strictly 'production') to remote server via SFTP.
|
||||||
|
Ensures existing production user data, SQLite databases, and dynamic uploads are NEVER modified.
|
||||||
|
"""
|
||||||
|
start_t = time.time()
|
||||||
|
ws = Path(workspace_path).expanduser().resolve()
|
||||||
|
if not ws.exists() or not ws.is_dir():
|
||||||
|
err = f"دایرکتوری محلی پروژه در مسیر {workspace_path} یافت نشد."
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project_name,
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="push",
|
||||||
|
requester=requester,
|
||||||
|
status="failed",
|
||||||
|
details=f"Push branch={branch} to {host}:{port}{remote_path}",
|
||||||
|
result_summary=err,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": err,
|
||||||
|
"files_uploaded": 0,
|
||||||
|
"files_skipped": 0,
|
||||||
|
"bytes_transferred": 0,
|
||||||
|
"duration": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run_push():
|
||||||
|
client = None
|
||||||
|
temp_export_dir = None
|
||||||
|
uploaded_files = []
|
||||||
|
skipped_files = []
|
||||||
|
total_bytes = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Extract strictly from production git branch
|
||||||
|
source_dir = ws
|
||||||
|
if (ws / ".git").exists():
|
||||||
|
try:
|
||||||
|
branches_proc = subprocess.run(
|
||||||
|
["git", "branch", "--list", branch],
|
||||||
|
cwd=str(ws),
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
target_branch = branch if branch in branches_proc.stdout else "HEAD"
|
||||||
|
|
||||||
|
temp_export_dir = Path(tempfile.mkdtemp(prefix="ssh_push_prod_"))
|
||||||
|
archive_proc = subprocess.Popen(
|
||||||
|
["git", "archive", target_branch],
|
||||||
|
cwd=str(ws),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
tar_proc = subprocess.Popen(
|
||||||
|
["tar", "-x", "-C", str(temp_export_dir)],
|
||||||
|
stdin=archive_proc.stdout,
|
||||||
|
)
|
||||||
|
archive_proc.stdout.close()
|
||||||
|
tar_proc.communicate()
|
||||||
|
|
||||||
|
if tar_proc.returncode == 0:
|
||||||
|
source_dir = temp_export_dir
|
||||||
|
except Exception as ge:
|
||||||
|
logger.warning(f"Git archive export failed for {branch} (using ws): {ge}")
|
||||||
|
source_dir = ws
|
||||||
|
|
||||||
|
client = self._create_ssh_client(
|
||||||
|
host=host.strip(),
|
||||||
|
port=port,
|
||||||
|
user=user.strip(),
|
||||||
|
password=password,
|
||||||
|
key_content_or_path=key,
|
||||||
|
timeout=25,
|
||||||
|
)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
# Prepare remote path
|
||||||
|
target_root = (remote_path or "/").strip().replace("\\", "/")
|
||||||
|
if not target_root.startswith("/"):
|
||||||
|
target_root = "/" + target_root
|
||||||
|
target_root = target_root.rstrip("/")
|
||||||
|
if not target_root:
|
||||||
|
target_root = "/"
|
||||||
|
|
||||||
|
def ensure_remote_dir(r_dir: str):
|
||||||
|
if r_dir in ("/", ""):
|
||||||
|
return
|
||||||
|
parts = [p for p in r_dir.split("/") if p]
|
||||||
|
curr = ""
|
||||||
|
for p in parts:
|
||||||
|
curr += "/" + p
|
||||||
|
try:
|
||||||
|
sftp.stat(curr)
|
||||||
|
except IOError:
|
||||||
|
try:
|
||||||
|
sftp.mkdir(curr)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ensure_remote_dir(target_root)
|
||||||
|
|
||||||
|
# Scan and filter local files
|
||||||
|
for root, dirs, files in os.walk(str(source_dir)):
|
||||||
|
rel_dir = os.path.relpath(root, str(source_dir))
|
||||||
|
if rel_dir == ".":
|
||||||
|
rel_dir = ""
|
||||||
|
|
||||||
|
dirs_to_keep = []
|
||||||
|
for d in dirs:
|
||||||
|
dir_rel = f"{rel_dir}/{d}".strip("/")
|
||||||
|
if should_exclude_ssh(dir_rel, custom_excludes):
|
||||||
|
skipped_files.append(dir_rel + "/")
|
||||||
|
else:
|
||||||
|
dirs_to_keep.append(d)
|
||||||
|
dirs[:] = dirs_to_keep
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
file_rel = f"{rel_dir}/{f}".strip("/")
|
||||||
|
if should_exclude_ssh(file_rel, custom_excludes):
|
||||||
|
skipped_files.append(file_rel)
|
||||||
|
else:
|
||||||
|
local_file = Path(root) / f
|
||||||
|
remote_file_path = f"{target_root}/{file_rel}".replace("//", "/")
|
||||||
|
remote_file_dir = os.path.dirname(remote_file_path)
|
||||||
|
|
||||||
|
ensure_remote_dir(remote_file_dir)
|
||||||
|
sftp.put(str(local_file), remote_file_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
f_size = local_file.stat().st_size
|
||||||
|
total_bytes += f_size
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
uploaded_files.append(file_rel)
|
||||||
|
|
||||||
|
sftp.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
dur = time.time() - start_t
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"files_uploaded": len(uploaded_files),
|
||||||
|
"files_skipped": len(skipped_files),
|
||||||
|
"bytes_transferred": total_bytes,
|
||||||
|
"duration": dur,
|
||||||
|
"uploaded_list": uploaded_files,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SSH push failed: {e}", exc_info=True)
|
||||||
|
dur = time.time() - start_t
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"خطا در ارسال فایلها به سرور SSH: {e}",
|
||||||
|
"files_uploaded": len(uploaded_files),
|
||||||
|
"files_skipped": len(skipped_files),
|
||||||
|
"bytes_transferred": total_bytes,
|
||||||
|
"duration": dur,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
try:
|
||||||
|
client.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if temp_export_dir and os.path.exists(temp_export_dir):
|
||||||
|
shutil.rmtree(temp_export_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
res = await asyncio.to_thread(_run_push)
|
||||||
|
|
||||||
|
# Audit log record
|
||||||
|
status_str = "success" if res.get("success") else "failed"
|
||||||
|
summary_str = f"Uploaded {res.get('files_uploaded', 0)} files ({res.get('bytes_transferred', 0)} bytes)" if res.get("success") else str(res.get("error", "Error"))
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project_name,
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="push",
|
||||||
|
requester=requester,
|
||||||
|
status=status_str,
|
||||||
|
details=f"Push to {host}:{port}{remote_path} (Branch: {branch})",
|
||||||
|
result_summary=summary_str[:200],
|
||||||
|
duration_ms=res.get("duration", 0.0) * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
async def pull_project(
|
||||||
|
self,
|
||||||
|
workspace_path: str,
|
||||||
|
host: str,
|
||||||
|
port: int = 22,
|
||||||
|
user: str = "",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
remote_path: str = "/",
|
||||||
|
branch: str = "production",
|
||||||
|
custom_excludes: Optional[List[str]] = None,
|
||||||
|
user_id: int = 0,
|
||||||
|
project_name: str = "",
|
||||||
|
requester: str = "user_button",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Pulls files from the remote SSH server into local workspace / production branch.
|
||||||
|
Skips remote database and user upload files according to exclusion rules.
|
||||||
|
"""
|
||||||
|
start_t = time.time()
|
||||||
|
ws = Path(workspace_path).expanduser().resolve()
|
||||||
|
ws.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _run_pull():
|
||||||
|
client = None
|
||||||
|
downloaded_files = []
|
||||||
|
skipped_files = []
|
||||||
|
total_bytes = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = self._create_ssh_client(
|
||||||
|
host=host.strip(),
|
||||||
|
port=port,
|
||||||
|
user=user.strip(),
|
||||||
|
password=password,
|
||||||
|
key_content_or_path=key,
|
||||||
|
timeout=25,
|
||||||
|
)
|
||||||
|
sftp = client.open_sftp()
|
||||||
|
|
||||||
|
target_root = (remote_path or "/").strip().replace("\\", "/").rstrip("/")
|
||||||
|
if not target_root:
|
||||||
|
target_root = "/"
|
||||||
|
|
||||||
|
def recursive_download(r_dir: str, rel_dir: str = ""):
|
||||||
|
nonlocal total_bytes
|
||||||
|
try:
|
||||||
|
entries = sftp.listdir_attr(r_dir)
|
||||||
|
except IOError as e:
|
||||||
|
logger.warning(f"Cannot list remote dir {r_dir}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
fname = entry.filename
|
||||||
|
if fname in (".", ".."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur_rel = f"{rel_dir}/{fname}".strip("/")
|
||||||
|
cur_remote = f"{r_dir}/{fname}".replace("//", "/")
|
||||||
|
|
||||||
|
# Check if directory
|
||||||
|
import stat
|
||||||
|
is_dir = stat.S_ISDIR(entry.st_mode)
|
||||||
|
|
||||||
|
if should_exclude_ssh(cur_rel if not is_dir else cur_rel + "/", custom_excludes):
|
||||||
|
skipped_files.append(cur_rel + ("/" if is_dir else ""))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_dir:
|
||||||
|
local_sub = ws / cur_rel
|
||||||
|
local_sub.mkdir(parents=True, exist_ok=True)
|
||||||
|
recursive_download(cur_remote, cur_rel)
|
||||||
|
else:
|
||||||
|
local_dest = ws / cur_rel
|
||||||
|
local_dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
sftp.get(cur_remote, str(local_dest))
|
||||||
|
total_bytes += entry.st_size
|
||||||
|
downloaded_files.append(cur_rel)
|
||||||
|
|
||||||
|
recursive_download(target_root)
|
||||||
|
sftp.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
dur = time.time() - start_t
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"files_downloaded": len(downloaded_files),
|
||||||
|
"files_skipped": len(skipped_files),
|
||||||
|
"bytes_transferred": total_bytes,
|
||||||
|
"duration": dur,
|
||||||
|
"downloaded_list": downloaded_files,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SSH pull failed: {e}", exc_info=True)
|
||||||
|
dur = time.time() - start_t
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"خطا در دریافت فایلها از سرور SSH: {e}",
|
||||||
|
"files_downloaded": len(downloaded_files),
|
||||||
|
"files_skipped": len(skipped_files),
|
||||||
|
"bytes_transferred": total_bytes,
|
||||||
|
"duration": dur,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
try:
|
||||||
|
client.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
res = await asyncio.to_thread(_run_pull)
|
||||||
|
|
||||||
|
# Audit log record
|
||||||
|
status_str = "success" if res.get("success") else "failed"
|
||||||
|
summary_str = f"Downloaded {res.get('files_downloaded', 0)} files ({res.get('bytes_transferred', 0)} bytes)" if res.get("success") else str(res.get("error", "Error"))
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project_name,
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="pull",
|
||||||
|
requester=requester,
|
||||||
|
status=status_str,
|
||||||
|
details=f"Pull from {host}:{port}{remote_path} to {workspace_path}",
|
||||||
|
result_summary=summary_str[:200],
|
||||||
|
duration_ms=res.get("duration", 0.0) * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
async def execute_command(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
port: int = 22,
|
||||||
|
user: str = "",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
remote_path: str = "/",
|
||||||
|
command: str = "",
|
||||||
|
timeout: int = 60,
|
||||||
|
user_id: int = 0,
|
||||||
|
project_name: str = "",
|
||||||
|
requester: str = "user_button",
|
||||||
|
) -> Tuple[bool, str, int]:
|
||||||
|
"""
|
||||||
|
Executes a bash command remotely inside the specified remote_path directory.
|
||||||
|
"""
|
||||||
|
start_t = time.time()
|
||||||
|
if not command or not command.strip():
|
||||||
|
return False, "دستور اجرایی خالی است.", -1
|
||||||
|
|
||||||
|
def _run_exec():
|
||||||
|
client = None
|
||||||
|
try:
|
||||||
|
client = self._create_ssh_client(
|
||||||
|
host=host.strip(),
|
||||||
|
port=port,
|
||||||
|
user=user.strip(),
|
||||||
|
password=password,
|
||||||
|
key_content_or_path=key,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
cd_prefix = f"cd {remote_path} && " if remote_path and remote_path != "/" else ""
|
||||||
|
full_cmd = f"bash -c {subprocess.list2cmdline([cd_prefix + command])}" if cd_prefix else command
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command(full_cmd, timeout=timeout)
|
||||||
|
out = stdout.read().decode("utf-8", errors="ignore")
|
||||||
|
err = stderr.read().decode("utf-8", errors="ignore")
|
||||||
|
exit_code = stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
combined = ""
|
||||||
|
if out:
|
||||||
|
combined += out
|
||||||
|
if err:
|
||||||
|
if combined:
|
||||||
|
combined += "\n"
|
||||||
|
combined += f"[STDERR]\n{err}"
|
||||||
|
|
||||||
|
if not combined:
|
||||||
|
combined = f"(دستور با کد {exit_code} بدون خروجی اجرا شد)"
|
||||||
|
|
||||||
|
# Limit output size to prevent overflow
|
||||||
|
if len(combined) > 3500:
|
||||||
|
combined = combined[:3500] + "\n... (خروجی طولانی کوتاه شد)"
|
||||||
|
|
||||||
|
return (exit_code == 0), combined, exit_code
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"SSH command execution failed: {e}")
|
||||||
|
return False, f"خطا در اجرای فرمان SSH: {e}", -1
|
||||||
|
finally:
|
||||||
|
if client:
|
||||||
|
try:
|
||||||
|
client.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
success, output, code = await asyncio.to_thread(_run_exec)
|
||||||
|
dur = (time.time() - start_t) * 1000
|
||||||
|
|
||||||
|
# Audit log record
|
||||||
|
remote_audit.log_operation(
|
||||||
|
user_id=user_id,
|
||||||
|
project_name=project_name,
|
||||||
|
service_type="ssh",
|
||||||
|
action_type="exec",
|
||||||
|
requester=requester,
|
||||||
|
status="success" if success else "failed",
|
||||||
|
details=f"cmd: {command[:200]}",
|
||||||
|
result_summary=f"exit_code={code}, output={output[:100]}",
|
||||||
|
duration_ms=dur,
|
||||||
|
)
|
||||||
|
|
||||||
|
return success, output, code
|
||||||
|
|
||||||
|
|
||||||
|
# Global singleton instance
|
||||||
|
ssh_manager = SSHManager()
|
||||||
Reference in New Issue
Block a user