pax_global_header00006660000000000000000000000064150051452650014515gustar00rootroot0000000000000052 comment=c4aab60794bc3acbe6f5f70e55dfd53cbc4fb894 passim-0.1.10/000077500000000000000000000000001500514526500130705ustar00rootroot00000000000000passim-0.1.10/.clang-format000066400000000000000000000024261500514526500154470ustar00rootroot00000000000000--- AlignAfterOpenBracket: 'Align' AlignConsecutiveAssignments: 'false' AlignConsecutiveDeclarations: 'false' AlignConsecutiveMacros: 'true' AlignOperands: 'true' AlignTrailingComments: 'true' AllowAllArgumentsOnNextLine: 'false' AllowAllParametersOfDeclarationOnNextLine: 'false' AllowShortBlocksOnASingleLine: 'false' AllowShortCaseLabelsOnASingleLine: 'false' AllowShortFunctionsOnASingleLine: 'Inline' AllowShortIfStatementsOnASingleLine: 'false' AlwaysBreakAfterReturnType: 'All' BinPackParameters: 'false' BinPackArguments: 'false' BreakBeforeBraces: 'Linux' ColumnLimit: '100' DerivePointerAlignment: 'false' IndentCaseLabels: 'false' IndentWidth: '8' IncludeBlocks: 'Regroup' KeepEmptyLinesAtTheStartOfBlocks: 'false' MaxEmptyLinesToKeep: '1' PointerAlignment: 'Right' SortIncludes: 'true' SpaceAfterCStyleCast: 'false' SpaceBeforeAssignmentOperators : 'true' SpaceBeforeParens: 'ControlStatements' SpaceInEmptyParentheses: 'false' SpacesInSquareBrackets: 'false' TabWidth: '8' UseTab: 'Always' PenaltyBreakAssignment: '3' PenaltyBreakBeforeFirstCallParameter: '15' --- Language: 'Proto' --- Language: 'Cpp' IncludeCategories: - Regex: '^"config.h"$' Priority: '0' - Regex: '^<' Priority: '2' - Regex: '.*' Priority: '4' ... passim-0.1.10/.github/000077500000000000000000000000001500514526500144305ustar00rootroot00000000000000passim-0.1.10/.github/dependabot.yml000066400000000000000000000001661500514526500172630ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" passim-0.1.10/.github/workflows/000077500000000000000000000000001500514526500164655ustar00rootroot00000000000000passim-0.1.10/.github/workflows/main.yml000066400000000000000000000010411500514526500201300ustar00rootroot00000000000000name: Build and Test on: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read jobs: build-linux: runs-on: ubuntu-latest strategy: matrix: distro: - fedora - debian fail-fast: false steps: - uses: actions/checkout@v4 - run: docker build -t passim-${{ matrix.distro }} -f contrib/ci/Dockerfile-${{ matrix.distro }} . - run: docker run -t -v `pwd`:/build passim-${{ matrix.distro }} ./contrib/ci/build-${{ matrix.distro }}.sh passim-0.1.10/.github/workflows/scorecard.yml000066400000000000000000000060651500514526500211640ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '16 18 * * 0' push: branches: [ "main" ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: sarif_file: results.sarif passim-0.1.10/LICENSE000066400000000000000000000636361500514526500141130ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! passim-0.1.10/README.md000066400000000000000000000236731500514526500143620ustar00rootroot00000000000000# Passim [![Translation status](https://hosted.weblate.org/widget/passim/svg-badge.svg)](https://hosted.weblate.org/engage/passim/) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/hughsie/passim/badge)](https://securityscorecards.dev/viewer/?uri=github.com/hughsie/passim) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8814/badge)](https://www.bestpractices.dev/projects/8814) A local caching server. Named after the Latin word for “here, there and everywhere”. ## Introduction Much of the software running on your computer that connects to other systems over the Internet needs to periodically download *metadata* or information needed to perform other requests. As part of running the passim/LVFS projects I've seen how *download this small file once per 24h* turns into tens of millions of requests per day. Everybody downloads the same file from a CDN, and although a CDN is not super-expensive, it's certainly not free. Everybody on your current network (perhaps thousands of users) has to download the same 1MB blob of metadata from a CDN over a perhaps-expensive internet link. What if we could download the file from the CDN on one machine, and the next machine on the local network that needs it instead downloads it from the first machine? We could put a limit on the number of times it can be shared, and the maximum age so that we don't store yesterdays metadata forever, and so that we don't turn a ThinkPad X220 into a machine distributing 1Gb/s to every other machine in the office. We could cut the CDN traffic by at least one order of magnitude, but possibly much more. This is better for the person paying the cloud bill, the person paying for the internet connection, and the planet as a whole. This is what `passim` tries to be. You add automatically or manually add files to the daemon which stores them in `/var/lib/passim/data` with attributes set on each file for the `max-age` and `share-limit`. When the file has been shared more than the share limit number of times, or is older than the max age it is deleted and not advertised to other clients. The daemon then advertises the availability of the file as a mDNS service subtype and provides a tiny single-threaded webserver that supplies the file using HTTP using a self-signed TLS certificate. The file is sent when requested from a URL like `https://192.168.1.1:27500/filename.xml.gz?sha256=hash` -- any file requested without the checksum will not be supplied. Although this is a chicken-and-egg problem where you don't know the payload checksum until you've checked the remote server, this is easy solved using a tiny <100 byte request to the CDN for the payload checksum (or a .jcat file) and then the multi-megabyte (or multi-gigabyte!) payload can be requested over mDNS. ## Sharing Considerations Here we've assuming your local network (aka LAN) is a nice and friendly place, without evil people trying to overwhelm your system or feed you fake files. Although we request files by their hash (and thus can detect tampering) it still uses resources to send a file over the network. We'll assume that any network with working mDNS (as implemented in Avahi) is good enough to get metadata from other peers. If Avahi is not running, or mDNS is turned off on the firewall then no files will be shared. The cached index is available locally without any kind of authentication -- both over mDNS and as a webpage on `https://localhost:27500/`. So, **NEVER ADD FILES TO THE CACHE THAT YOU DO NOT WANT TO SHARE**. Even the filename may give more clues than you wanted to provide, e.g. sharing a file might get you in trouble. This can be subtle; if you download a security update for a Lenovo P1 Gen 3 laptop and share it with other laptops on your LAN -- it also tells the attacker your laptop model and also that you're running a system firmware that isn't patched against the latest firmware bug. My recommendation here is only to advertise files that are common to all machines. For instance: * AdBlocker metadata * Firmware update metadata * Remote metadata for update frameworks, e.g. apt-get/dnf etc. ## Implementation Considerations Any client **MUST** (and I'll go as far as to say it again, **MUST**) calculate the checksum of the supplied file and verify that it matches. There is no authentication or signing verification done so this step is non-optional. A malicious server could advertise the hash of `firmware.xml.gz` but actually supply `evil-payload.exe` -- and you do not want that. ## Static Data Passim can also add the contents of static directories, and offer those to clients. This might be useful if you have a big directory of thousands of files (an LVFS mirror, for example) that you want to distribute from a dedicated machine. To set this up, create a keyfile with a unique name and extension `.conf`, something like `/etc/passim.d/lvfs.conf` with the contents: [passim] Path=/srv/lvfs/downloads/ The running daemon will be notified this file has been created, scan the contents, and publish them for other users. If `passimd` has write permissions on the directory, it will also write an xattr of `user.checksum.sha256` which will speed up the next daemon restart considerably. ## Firewall Configuration Port 27500 should be open by default, but if downloading files fails you can open the port using firewalld: $ firewall-cmd --permanent --zone=public --add-port=27500/tcp ## Comparisons The obvious comparison to make is IPFS. I'll try to make this as fair as possible, although I'm obviously somewhat biased. IPFS: * existing project that's existed for many years tested by many people * allows sharing with other users not on your local network * not packaged in many distributions and not trivial to install correctly * requires a significant time to find resources * does not prioritize local clients over remote clients * requires a internet<->IPFS "gateway" which cost $$$ for a large number of files Passim: * new project that's not even finished * only allowed sharing with computers on your local network * returns results within 2s One concern we had specifically with IPFS with firmware were ITAR/EAR legal considerations. e.g. we can't share firmware containing strong encryption with users in some countries. From an ITAR/EAR point of view Passim would be compliant (as it only shares locally) and IPFS would not be. ## Debugging $ curl -v -k https://localhost:27500/HELLO.md?sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 -L * Trying 127.0.0.1:27500... * Connected to localhost (127.0.0.1) port 27500 (#0) * ALPN: offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN: server accepted http/1.1 * Server certificate: * subject: [NONE] * start date: Aug 15 20:13:03 2023 GMT * expire date: Dec 31 23:59:59 9999 GMT * using HTTP/1.1 > GET /HELLO.md?sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 HTTP/1.1 > Host: localhost:27500 > User-Agent: curl/8.0.1 > Accept: */* > < HTTP/1.1 302 Found < Server: passim libsoup/3.4.2 < Date: Tue, 15 Aug 2023 20:37:10 GMT < Location: https://192.168.122.39:27500/sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447?sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 < Content-Type: text/html < Content-Length: 227 < * Ignoring the response-body * Connection #0 to host localhost left intact * Issue another request to this URL: 'https://192.168.122.39:27500/sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447?sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447' * Trying 192.168.122.39:27500... * Connected to 192.168.122.39 (192.168.122.39) port 27500 (#1) * ALPN: offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN: server accepted http/1.1 * Server certificate: * subject: [NONE] * start date: Aug 15 20:27:28 2023 GMT * expire date: Dec 31 23:59:59 9999 GMT * using HTTP/1.1 > GET /sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447?sha256=a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447 HTTP/1.1 > Host: 192.168.122.39:27500 > User-Agent: curl/8.0.1 > Accept: */* > < HTTP/1.1 200 OK < Server: passim libsoup/3.4.2 < Date: Tue, 15 Aug 2023 20:37:11 GMT < Content-Disposition: attachment; filename="a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447-HELLO.md" < Content-Type: text/markdown < Content-Length: 12 < hello world * Connection #1 to host 192.168.122.39 left intact Using the CLI: $ passim dump 7ea83bf1f9505f1e846cddef0d8ee49f6fd19361e2eb2f2e4f371b86382eacc2 HELLO.md (max-age: 86400, share-limit: 5) $ sudo passim publish /var/lib/passim/metadata/lvfs/metadata.xml.xz 60 44 7ea83bf1f9505f1e846cddef0d8ee49f6fd19361e2eb2f2e4f371b86382eacc2 HELLO.md (max-age: 86400, share-limit: 5) 0157efe3cdab369a17b68facb187df1c559c91e2771c9094880ff2019ad84eaf metadata.xml.xz (max-age: 60, share-count: 0, share-limit: 44) # TODO: - Self tests passim-0.1.10/RELEASE000066400000000000000000000015541500514526500141000ustar00rootroot00000000000000Passim Release Notes Write release entries: git log --format="%s" --cherry-pick --right-only 0.1.9... | grep -i -v trivial | grep -v Merge | sort | uniq Add any user visible changes into ../data/org.freedesktop.Passim.metainfo.xml appstream-util appdata-to-news ../data/org.freedesktop.Passim.metainfo.xml > NEWS Update translations: ninja-build passim-pot git commit -a -m "trivial: Update translations for Weblate" # MAKE SURE THIS IS CORRECT export release_ver="0.1.10" git commit -a -m "Release ${release_ver}" --no-verify git tag -s -f -m "Release ${release_ver}" "${release_ver}" ninja dist git push --tags git push gpg -b -a meson-dist/passim-${release_ver}.tar.xz Create release and upload tarball to https://github.com/hughsie/passim/tags Do post release version bump in meson.build git commit -a -m "trivial: post release version bump" --no-verify git push passim-0.1.10/SECURITY.md000066400000000000000000000004771500514526500146710ustar00rootroot00000000000000# Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 0.1.x | :white_check_mark: | ## Reporting a Vulnerability We have enabled private reporting in GitHub, so please [follow these steps](https://github.com/hughsie/passim/security) to report vulnerabilities. passim-0.1.10/contrib/000077500000000000000000000000001500514526500145305ustar00rootroot00000000000000passim-0.1.10/contrib/ci/000077500000000000000000000000001500514526500151235ustar00rootroot00000000000000passim-0.1.10/contrib/ci/Dockerfile-debian000066400000000000000000000007011500514526500203330ustar00rootroot00000000000000FROM debian:unstable RUN apt-get update -qq RUN apt-get install -yq --no-install-recommends \ gnutls-dev \ gobject-introspection \ libgirepository1.0-dev \ libglib2.0-bin \ libglib2.0-dev \ libsoup-3.0-dev \ libsystemd-dev \ meson \ ninja-build \ pkg-config \ systemd-dev \ shared-mime-info # Meson is too old in unstable, and that won't change until Buster is released # RUN pip3 install meson --break-system-packages WORKDIR /build passim-0.1.10/contrib/ci/Dockerfile-fedora000066400000000000000000000003171500514526500203540ustar00rootroot00000000000000FROM fedora:38 RUN dnf -y update RUN dnf -y install \ git-core \ gnutls-devel \ gobject-introspection-devel \ libsoup3-devel \ meson \ redhat-rpm-config \ shared-mime-info \ systemd WORKDIR /build passim-0.1.10/contrib/ci/build-debian.sh000077500000000000000000000002421500514526500177770ustar00rootroot00000000000000#!/bin/sh set -e export LC_ALL=C.UTF-8 mkdir -p build && cd build rm -rf * meson .. ninja -v || bash ninja test -v DESTDIR=/tmp/install-ninja ninja install cd .. passim-0.1.10/contrib/ci/build-fedora.sh000077500000000000000000000002421500514526500200150ustar00rootroot00000000000000#!/bin/sh set -e export LC_ALL=C.UTF-8 mkdir -p build && cd build rm -rf * meson .. ninja -v || bash ninja test -v DESTDIR=/tmp/install-ninja ninja install cd .. passim-0.1.10/contrib/passim.spec.in000066400000000000000000000053701500514526500173120ustar00rootroot00000000000000%global glib2_version 2.45.8 %global systemd_version 231 %define alphatag #ALPHATAG# %global __meson_wrap_mode nodownload Summary: Local caching server Name: passim Version: #VERSION# Release: 0.#BUILD#%{?alphatag}%{?dist} License: LGPL-2.1-or-later URL: https://github.com/hughsie/%{name} Source0: https://github.com/hughsie/%{name}/releases/download/%{version}/%{name}-%{version}.tar.xz BuildRequires: gcc BuildRequires: gettext BuildRequires: git-core BuildRequires: glib2-devel >= %{glib2_version} BuildRequires: gnutls-devel BuildRequires: gobject-introspection-devel BuildRequires: libappstream-glib BuildRequires: libsoup3-devel BuildRequires: meson BuildRequires: systemd-rpm-macros BuildRequires: systemd >= %{systemd_version} Recommends: avahi Requires: glib2%{?_isa} >= %{glib2_version} Requires: %{name}-libs%{?_isa} = %{version}-%{release} # Obsolete versions from before the subpackage split Obsoletes: %{name} < 0.1.1-3 %description Passim is a daemon that allows software to share files on your local network. %package libs Summary: Local caching server library # Obsolete versions from before the subpackage split Obsoletes: %{name} < 0.1.1-3 %description libs libpassim is a library that allows software to share files on your local network using the passimd daemon. %package devel Summary: Development package for %{name} Requires: %{name}%{?_isa} = %{version}-%{release} %description devel Files for development with %{name}. %prep %autosetup -p1 %build %meson %meson_build %install %meson_install rm %{buildroot}/var/lib/passim/data/* %find_lang %{name} %check %meson_test appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/*.metainfo.xml %post %systemd_post passim.service %preun %systemd_preun passim.service %postun %systemd_postun_with_restart passim.service %files -f %{name}.lang %doc README.md %license LICENSE %{_bindir}/passim %config(noreplace)%{_sysconfdir}/passim.conf %dir %{_datadir}/passim %{_datadir}/passim/*.ico %{_datadir}/passim/*.css %{_datadir}/dbus-1/system.d/org.freedesktop.Passim.conf %{_datadir}/dbus-1/interfaces/org.freedesktop.Passim.xml %{_datadir}/dbus-1/system-services/org.freedesktop.Passim.service %{_datadir}/icons/hicolor/scalable/apps/org.freedesktop.Passim.svg %{_datadir}/icons/hicolor/256x256/apps/org.freedesktop.Passim.png %{_datadir}/metainfo/org.freedesktop.Passim.metainfo.xml %{_libdir}/girepository-1.0/Passim-1.0.typelib %{_libexecdir}/passimd %{_mandir}/man1/passim.1* %{_unitdir}/passim.service /usr/lib/sysusers.d/passim.conf %files libs %license LICENSE %{_libdir}/libpassim.so.1* %files devel %{_datadir}/gir-1.0/Passim-1.0.gir %dir %{_includedir}/passim-1 %{_includedir}/passim-1/passim*.h %{_libdir}/libpassim*.so %{_libdir}/pkgconfig/passim.pc %changelog %autochangelog passim-0.1.10/data/000077500000000000000000000000001500514526500140015ustar00rootroot00000000000000passim-0.1.10/data/a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447-HELLO.md000066400000000000000000000000141500514526500257120ustar00rootroot00000000000000hello world passim-0.1.10/data/favicon.ico000066400000000000000000000072561500514526500161340ustar00rootroot00000000000000  ( 8    vq~Ё|Ҁڂր}{y{~怙kz#     b} z|Œɷ͊~       z|ϻt:      o(щзŵѸ̉         {ײӟĕ|i       nF˘Ғ۪q}    +|·՘̾нĬҍöί~   u_u-   )?GкݗƧҿؿʐ̝ɲ  yǥxO   kٹdzރkr{  ˈѼ̾ݼѳ֚КԩԊy|*۫ޛn}Âңںʼȟ֞Ʈ˷|ؼ׹ޱ{t:ׯ̼ܾыٰh|~ٷѯ͢ݜرʳކĽ{ `wy}ѱȿTjv  pn}ӟиɡϯ™c   '9uĻ֗ɒ& rA܅̕ϲuA    {Ɵèڶڳ͎}    ^}ؙg&      y{ŞŰ٠ºóz       ?fa╪Þۺ`y        }[߸h2         w           z0}}||؁xVgx*  ??passim-0.1.10/data/meson.build000066400000000000000000000023561500514526500161510ustar00rootroot00000000000000install_data( 'passim.conf', install_dir: sysconfdir, ) install_data( 'passim.sysusers.conf', rename: ['passim.conf'], install_dir: sysusersdir, ) install_data( 'favicon.ico', 'style.css', install_dir: datadir / meson.project_name(), ) install_data( 'org.freedesktop.Passim.svg', install_dir: datadir / 'icons/hicolor/scalable/apps', ) install_data( 'org.freedesktop.Passim.png', install_dir: datadir / 'icons/hicolor/256x256/apps', ) con2 = configuration_data() con2.set('libexecdir', libexecdir) con2.set('localstatedir', localstatedir) configure_file( input: 'passim.service.in', output: 'passim.service', configuration: con2, install: true, install_dir: systemdunitdir, ) configure_file( input: 'org.freedesktop.Passim.service.in', output: 'org.freedesktop.Passim.service', configuration: con2, install: true, install_dir: datadir / 'dbus-1/system-services', ) install_data( 'org.freedesktop.Passim.conf', install_dir: datadir / 'dbus-1/system.d', ) install_data( 'org.freedesktop.Passim.metainfo.xml', install_dir: datadir / 'metainfo', ) install_data( 'a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447-HELLO.md', install_dir: localstatedir / 'lib' / meson.project_name() / 'data', ) passim-0.1.10/data/org.freedesktop.Passim.conf000066400000000000000000000022041500514526500212020ustar00rootroot00000000000000 passim-0.1.10/data/org.freedesktop.Passim.metainfo.xml000066400000000000000000000124211500514526500226600ustar00rootroot00000000000000 org.freedesktop.Passim CC0-1.0 LGPL-2.1-or-later Passim Local caching server

Passim is a daemon that allows other software to share metadata on your local network.

https://github.com/hughsie/passim/issues https://github.com/hughsie/passim moderate passim

This release fixes the following bugs:

  • Adjust man page to parse properly in tools like lexgrog
  • Fix a crash when reaching the share limit
  • Fix the location the icon is installed to match the correct size
  • Restart service after failure

This release fixes the following bugs:

  • Install the icons to the correct location

This release fixes the following bugs:

  • Fix an almost-impossible resource leak when adding files

This release fixes the following bugs:

  • Fix a crash when deleting items
  • Fix a small memory leak when parsing a request with duplicate arguments
  • Lock the systemd service down some more

This release adds the following features:

  • Add a 'download' command to the CLI, and allow ignoring the localhost scan
  • Log to an audit log when publishing, unpublishing and sharing
  • Show the URI, auto-generated name, network and carbon saving in the CLI

This release fixes the following bugs:

  • Add translation support for the CLI tool
  • Properly encode socket addresses, but disable IPv6 by default
  • Redirect with the basename set correctly
  • Reduce some log-spam when checking item ages

This release adds some new API for fwupd to use.

  • Add passim_item_set_stream()

This release fixes the following bugs:

  • Allow setting MaxItemSize bigger than 4GB
  • Do not abort with a critical warning when no query is used
  • Do not follow symlinks in libdir and sysconfpkgdir
  • Fix a harmless assertion warning when serving a zero-length file
  • Properly escape the Content-Disposition filename
  • Reduce RSS when reloading the daemon
  • Show a better message when publishing a file that is too large

Many thanks to Matthias Gerstner from the SUSE Security team for the code review.

This release fixes the following bug:

  • Use a dedicated user to run the server

This release adds the following features:

  • Add file size information into the exported item
  • Allow admins to add a directory of static contents

This release fixes the following bugs:

  • Do not advertise files when on a metered network connection
  • Do not fail to start the service if /var/lib/passim/data does not exist
  • Explicitly depend on avahi-daemon in passim.service

This release fixes the following bugs:

  • Sanity check share-count is less than share-limit
  • Show the correct age in the 'passim dump' CLI

Initial release.

passim-0.1.10/data/org.freedesktop.Passim.png000066400000000000000000000106611500514526500210470ustar00rootroot00000000000000PNG  IHDR\rf pHYsetEXtSoftwarewww.inkscape.org<>IDATx[pםEsf$0!ŵl:,^opLcOU>xS8Yak>*8&S$M;AE`3fF}94hݣ* ӣ3ݧD$u(: "D1c @$H0` "D1c @$H0` "D1c @$u>zzNݣ4t'HD0y0`=~~uݺ x *[U+I+< `ud ! eh@]U#" S; eM41[(G}2w~U x!1#3?OZhP:VP?r'=\5C; P -LZ';?Q% ?>FP̓ 0j-t X8V(Fi^Z[C/jYwAW <|Z50GtWA %w Q<]!#kTNh4^( @$H0` "D1c @$H0` "D1 CA?$ɠk܄ 'DT0 ahn C}D *蝳=Q v56 @$H0` "D1c @$H0` "D1c @$H0` "D1c @$H0` "D1c @$H0` "D1c @$H0` "D1c @$H0` "Dm[A(ΔR4ts'͉DGAGt}lQ\YK$SA &)@>_%D53ЏFFFL@)dIQ(u##dA! Z2-pB,F:Q,dZP> V硵F& ~#@4T*ts3JΟ;x<\R mm0L3D m7!myh|PӕOk 4ѱv_o-$DC)B}?yzꍂ §~-,ZC)hf\.T2f(mmC9C$Id2ٲK~?C8)Ի`6nXvbX @qC2H&H$0>;Qm 5~V$e%c{} XPʀa(gZkU+mݰLK]m Tl?{kZ&i9GpI6=}zZݵ,( ?p^gqY\:]'ٸֳrOc S9Y3u$>RRƌ#M^ϴvЖPu̴Rk* 6Tz_f.T24]wo 6|y5/wD"Gwu|zp2~eر K5u^½kǧ×~xiwHw^{+R`3Z}2<O_5kVc󟽉k׆V`)` "DŶP(룓5/לN;7O[]q޾~\IMuƦ^ 3܁&^P ;1mJī׬B6忷fq]^pa/ L4kts 1{c"iؓ~P8vlUVb0Άư:NV/pOYG\5j=}5שFg遁K: Z1}Wko˚/hoMhii>׏.];:?/\)` "DŶ`Ѣ6̓׼\Rt/:˖݆ǿiWСw;ǧ ʃ/^z /qk_V\|ZQYfxrOkҞRP p62MXUSXV4ԌT)u'H9[yG= eN[w#_Z{S LO?b4%ˏ[[Hda\0X{) `=}pgŲQ:yzֶIYr :5/lvV^qvm~u::ڱ;yD\.w=2Bj[&kawz[o-*U[Bf~}rm0뙼؍TZ\xO\ E>SDp.Q 8Múboэ`jjSmz~ *'y =ؾ"y L]Ѐl3ȳD3'N@j5bO/0W4!DeA <>@$:Dq~wʷm) z8H_6,0J-[>VJAD R] J7ªG )SO=a -555}POh: l`3a ^իW_KGAԾBh>%rʳ "Dq6<{whEu7};Q;r J=avp$;?݀H{α18c]mZPߨRzgw t7PBYoŹ#xy֎CzwXkՑooL"(87>ެc0dq}߯r+@%I$S]DžM17ybxpqP*zcg&XcH&LcJɦ@0cԧߚ>,Ƙto_<0g% L~E-xbc;=O L x H$H0` "D1c 1ȁLR߀bᇴ^2ڵ!`߾}Ȉjim=:1X]Gm lBDu` @$H0` "D1c @$H0` "D1c @$H0` "D1c @$H0` "Vn5IENDB`passim-0.1.10/data/org.freedesktop.Passim.service.in000066400000000000000000000003001500514526500223150ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.Passim Documentation=https://github.com/hughsie/passim Exec=@libexecdir@/passimd User=passim SystemdService=passim.service AssumedAppArmorLabel=unconfined passim-0.1.10/data/org.freedesktop.Passim.svg000066400000000000000000000202161500514526500210570ustar00rootroot00000000000000 Passim passim-0.1.10/data/passim.conf000066400000000000000000000000631500514526500161430ustar00rootroot00000000000000[daemon] # Port = 27500 # Path = /some/other/place passim-0.1.10/data/passim.service.in000066400000000000000000000016201500514526500172630ustar00rootroot00000000000000[Unit] Description=Local Caching Server Documentation=https://github.com/hughsie/passim After=avahi-daemon.service Before=display-manager.service Wants=avahi-daemon.service [Service] Type=dbus TimeoutSec=180 BusName=org.freedesktop.Passim ExecStart=@libexecdir@/passimd User=passim DevicePolicy=closed LockPersonality=yes MemoryDenyWriteExecute=yes NoNewPrivileges=yes PrivateDevices=yes PrivateMounts=yes PrivateTmp=yes ProtectClock=yes ProtectControlGroups=yes ProtectHome=yes ProtectHostname=yes ProtectKernelLogs=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectSystem=full RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK RestrictNamespaces=yes RestrictRealtime=yes RestrictSUIDSGID=yes SystemCallFilter=@system-service SystemCallFilter=~@resources SystemCallErrorNumber=EPERM SystemCallArchitectures=native StateDirectory=passim passim/data LogsDirectory=passim Restart=on-failure passim-0.1.10/data/passim.sysusers.conf000066400000000000000000000001761500514526500200470ustar00rootroot00000000000000#Type Name ID GECOS Home directory Shell u passim - "Local Caching Server" /usr/share/empty - passim-0.1.10/data/style.css000066400000000000000000000004151500514526500156530ustar00rootroot00000000000000body { font-family: Arial, Helvetica, sans-serif; padding: 20px 20px 20px 20px; } th { text-align: left; } tr:nth-child(even) { background-color: #f2f2f2; } table, th, td { border: 1px solid #f2f2f2; border-collapse: collapse; } th, td { padding: 10px; } passim-0.1.10/libpassim/000077500000000000000000000000001500514526500150535ustar00rootroot00000000000000passim-0.1.10/libpassim/generate-version-script.py000077500000000000000000000107661500514526500222210ustar00rootroot00000000000000#!/usr/bin/python3 # pylint: disable=invalid-name,missing-docstring # # Copyright 2017 Richard Hughes # # SPDX-License-Identifier: LGPL-2.1-or-later import sys import argparse import xml.etree.ElementTree as ET XMLNS = "{http://www.gtk.org/introspection/core/1.0}" XMLNS_C = "{http://www.gtk.org/introspection/c/1.0}" def parse_version(ver): return tuple(map(int, ver.split("."))) def usage(return_code): """print usage and exit with the supplied return code""" if return_code == 0: out = sys.stdout else: out = sys.stderr out.write("usage: %s \n" % sys.argv[0]) sys.exit(return_code) class LdVersionScript: """Rasterize some text""" def __init__(self, library_name): self.library_name = library_name self.releases = {} self.overrides = {} def _add_node(self, node): identifier = node.attrib[XMLNS_C + "identifier"] introspectable = int(node.get("introspectable", 1)) version = node.get("version", None) if introspectable and not version: print("No version for", identifier) sys.exit(1) if not version: return None version = node.attrib["version"] if version not in self.releases: self.releases[version] = [] release = self.releases[version] if identifier not in release: release.append(identifier) return version def _add_cls(self, cls): # add all class functions for node in cls.findall(XMLNS + "function"): self._add_node(node) # choose the lowest version method for the _get_type symbol version_lowest = None # add all class methods for node in cls.findall(XMLNS + "method"): version_tmp = self._add_node(node) if version_tmp: if not version_lowest or parse_version(version_tmp) < parse_version( version_lowest ): version_lowest = version_tmp # add the constructor for node in cls.findall(XMLNS + "constructor"): version_tmp = self._add_node(node) if version_tmp: if not version_lowest or parse_version(version_tmp) < parse_version( version_lowest ): version_lowest = version_tmp if "{http://www.gtk.org/introspection/glib/1.0}get-type" not in cls.attrib: return type_name = cls.attrib["{http://www.gtk.org/introspection/glib/1.0}get-type"] # finally add the get_type symbol version = self.overrides.get(type_name, version_lowest) if version: self.releases[version].append(type_name) def import_gir(self, filename): tree = ET.parse(filename) root = tree.getroot() for ns in root.findall(XMLNS + "namespace"): for node in ns.findall(XMLNS + "function"): self._add_node(node) for cls in ns.findall(XMLNS + "record"): self._add_cls(cls) for cls in ns.findall(XMLNS + "class"): self._add_cls(cls) def render(self): # get a sorted list of all the versions versions = [] for version in self.releases: versions.append(version) # output the version data to a file verout = "# generated automatically, do not edit!\n" oldversion = None for version in sorted(versions, key=parse_version): symbols = sorted(self.releases[version]) verout += "\n%s_%s {\n" % (self.library_name, version) verout += " global:\n" for symbol in symbols: verout += " %s;\n" % symbol verout += " local: *;\n" if oldversion: verout += "} %s_%s;\n" % (self.library_name, oldversion) else: verout += "};\n" oldversion = version return verout if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( "-r", "--override", action="append", nargs=2, metavar=("symbol", "version") ) args, argv = parser.parse_known_args() if len(argv) != 3: usage(1) ld = LdVersionScript(library_name=argv[0]) if args.override: for override_symbol, override_version in args.override: ld.overrides[override_symbol] = override_version ld.import_gir(argv[1]) open(argv[2], "w").write(ld.render()) passim-0.1.10/libpassim/meson.build000066400000000000000000000057141500514526500172240ustar00rootroot00000000000000gnome = import('gnome') passim_version_h = configure_file( input: 'passim-version.h.in', output: 'passim-version.h', configuration: conf ) install_headers([ 'passim.h', 'passim-client.h', 'passim-item.h', passim_version_h, ], subdir: 'passim-1', ) libpassim_deps = [ libgio, ] libpassim_src = [ 'passim-client.c', 'passim-item.c', 'passim-version.c', ] passim_mapfile = 'passim.map' vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), passim_mapfile) passim = library( 'passim', sources: libpassim_src, soversion: libpassim_lt_current, version: libpassim_lt_version, dependencies: libpassim_deps, c_args: [ '-DG_LOG_DOMAIN="Passim"', '-DLOCALSTATEDIR="' + localstatedir + '"', ], include_directories: root_incdir, link_args: cc.get_supported_link_arguments([vflag]), link_depends: passim_mapfile, install: true ) passim_dep = declare_dependency( link_with: passim, include_directories: [root_incdir, include_directories('.')], dependencies: libpassim_deps ) pkgg = import('pkgconfig') pkgg.generate( passim, requires: [ 'gio-2.0' ], subdirs: 'passim-1', version: meson.project_version(), name: 'passim', filebase: 'passim', description: 'passim is a system daemon for installing device firmware', ) gir_dep = dependency('gobject-introspection-1.0', required: get_option('introspection')) introspection = get_option('introspection').disable_auto_if(host_machine.system() != 'linux').disable_auto_if(not gir_dep.found()) if introspection.allowed() passim_gir_deps = [ libgio, ] passim_gir = gnome.generate_gir(passim, sources: [ 'passim-client.c', 'passim-client.h', 'passim-item.c', 'passim-item.h', 'passim-version.c', passim_version_h, ], nsversion: '1.0', namespace: 'Passim', symbol_prefix: 'passim', identifier_prefix: ['Passim', 'passim'], export_packages: 'passim', header: 'passim.h', dependencies: passim_gir_deps, includes: [ 'Gio-2.0', 'GObject-2.0', ], install: true ) # Verify the map file is correct -- note we can't actually use the generated # file for two reasons: # # 1. We don't hard depend on GObject Introspection # 2. The map file is required to build the lib that the GIR is built from # # To avoid the circular dep, and to ensure we don't change exported API # accidentally actually check in a version of the version script to git. generate_version_script = [python3, files('generate-version-script.py')] mapfile_target = custom_target('passim_mapfile', input: passim_gir[0], output: 'passim.map', command: [ generate_version_script, 'LIBPASSIM', '@INPUT@', '@OUTPUT@', ], ) diffcmd = find_program('diff') test('passim-exported-api', diffcmd, args: [ '-urNp', files('passim.map'), mapfile_target, ], ) endif passim_incdir = include_directories('.') passim-0.1.10/libpassim/passim-client.c000066400000000000000000000311731500514526500177740ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #ifdef HAVE_MEMFD_CREATE #include #endif #include "passim-client.h" /** * PassimClient: * * A shared client. */ typedef struct { GDBusProxy *proxy; gchar *version; gchar *name; gchar *uri; PassimStatus status; guint64 download_saving; gdouble carbon_saving; } PassimClientPrivate; G_DEFINE_TYPE_WITH_PRIVATE(PassimClient, passim_client, G_TYPE_OBJECT) #define GET_PRIVATE(o) (passim_client_get_instance_private(o)) /** * passim_client_get_version: * @self: a #PassimClient * * Gets the daemon version. * * Returns: the version string, or %NULL if unset * * Since: 0.1.0 **/ const gchar * passim_client_get_version(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), NULL); return priv->version; } /** * passim_client_get_name: * @self: a #PassimClient * * Gets the daemon name. * * Returns: the name string, or %NULL if unset * * Since: 0.1.6 **/ const gchar * passim_client_get_name(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), NULL); return priv->name; } /** * passim_client_get_uri: * @self: a #PassimClient * * Gets the daemon URI. * * Returns: the URI string, or %NULL if unset * * Since: 0.1.6 **/ const gchar * passim_client_get_uri(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), NULL); return priv->uri; } /** * passim_client_get_status: * @self: a #PassimClient * * Gets the daemon status. * * Returns: the #PassimStatus * * Since: 0.1.2 **/ PassimStatus passim_client_get_status(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), PASSIM_STATUS_UNKNOWN); return priv->status; } /** * passim_client_get_download_saving: * @self: a #PassimClient * * Gets the total number of bytes saved from using this project. * * Returns: bytes * * Since: 0.1.6 **/ guint64 passim_client_get_download_saving(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), G_MAXUINT64); return priv->download_saving; } /** * passim_client_get_carbon_saving: * @self: a #PassimClient * * Gets the carbon saving from using this project. * * Returns: kgs of CO₂e * * Since: 0.1.6 **/ gdouble passim_client_get_carbon_saving(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), PASSIM_STATUS_UNKNOWN); return priv->carbon_saving; } static void passim_client_load_proxy_properties(PassimClient *self) { PassimClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GVariant) download_saving = NULL; g_autoptr(GVariant) carbon_saving = NULL; g_autoptr(GVariant) name = NULL; g_autoptr(GVariant) status = NULL; g_autoptr(GVariant) version = NULL; g_autoptr(GVariant) uri = NULL; version = g_dbus_proxy_get_cached_property(priv->proxy, "DaemonVersion"); if (version != NULL) { g_free(priv->version); priv->version = g_variant_dup_string(version, NULL); } name = g_dbus_proxy_get_cached_property(priv->proxy, "Name"); if (name != NULL) { g_free(priv->name); priv->name = g_variant_dup_string(name, NULL); } uri = g_dbus_proxy_get_cached_property(priv->proxy, "Uri"); if (uri != NULL) { g_free(priv->uri); priv->uri = g_variant_dup_string(uri, NULL); } status = g_dbus_proxy_get_cached_property(priv->proxy, "Status"); if (status != NULL) priv->status = g_variant_get_uint32(status); download_saving = g_dbus_proxy_get_cached_property(priv->proxy, "DownloadSaving"); if (download_saving != NULL) priv->download_saving = g_variant_get_uint64(download_saving); carbon_saving = g_dbus_proxy_get_cached_property(priv->proxy, "CarbonSaving"); if (carbon_saving != NULL) priv->carbon_saving = g_variant_get_double(carbon_saving); } static void passim_client_proxy_signal_cb(GDBusProxy *proxy, const gchar *sender_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { PassimClient *self = PASSIM_CLIENT(user_data); if (g_strcmp0(signal_name, "Changed") == 0) passim_client_load_proxy_properties(self); } /** * passim_client_load: * @self: a #PassimClient * @error: (nullable): optional return location for an error * * Loads properties from the passim daemon. * * Returns: %TRUE for success * * Since: 0.1.0 **/ gboolean passim_client_load(PassimClient *self, GError **error) { PassimClientPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_CLIENT(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (priv->proxy != NULL) return TRUE; priv->proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, PASSIM_DBUS_SERVICE, PASSIM_DBUS_PATH, PASSIM_DBUS_INTERFACE, NULL, error); if (priv->proxy == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); return FALSE; } g_signal_connect(G_DBUS_PROXY(priv->proxy), "g-signal", G_CALLBACK(passim_client_proxy_signal_cb), self); passim_client_load_proxy_properties(self); /* success */ return TRUE; } static GPtrArray * passim_item_array_from_variant(GVariant *value) { GPtrArray *items = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); g_autoptr(GVariant) untuple = g_variant_get_child_value(value, 0); gsize sz = g_variant_n_children(untuple); for (guint i = 0; i < sz; i++) { g_autoptr(GVariant) data = g_variant_get_child_value(untuple, i); g_ptr_array_add(items, passim_item_from_variant(data)); } return items; } /** * passim_client_get_items: * @self: a #PassimClient * @error: (nullable): optional return location for an error * * Get items currently published by the daemon. * * Returns: (element-type PassimItem) (transfer container): items, or %NULL for error * * Since: 0.1.0 **/ GPtrArray * passim_client_get_items(PassimClient *self, GError **error) { PassimClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GVariant) val = NULL; g_return_val_if_fail(PASSIM_IS_CLIENT(self), NULL); g_return_val_if_fail(priv->proxy != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); val = g_dbus_proxy_call_sync(priv->proxy, "GetItems", NULL, G_DBUS_CALL_FLAGS_NONE, 1500, NULL, error); if (val == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); return FALSE; } /* success */ return passim_item_array_from_variant(val); } /** * passim_client_unpublish: * @self: a #PassimClient * @hash: (not nullable): an item hash value * @error: (nullable): optional return location for an error * * Unpublish a file from the index. * * Returns: %TRUE for success * * Since: 0.1.0 **/ gboolean passim_client_unpublish(PassimClient *self, const gchar *hash, GError **error) { PassimClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GVariant) val = NULL; g_return_val_if_fail(PASSIM_IS_CLIENT(self), FALSE); g_return_val_if_fail(priv->proxy != NULL, FALSE); g_return_val_if_fail(hash != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); val = g_dbus_proxy_call_sync(priv->proxy, "Unpublish", g_variant_new("(s)", hash), G_DBUS_CALL_FLAGS_NONE, 1500, NULL, error); if (val == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); return FALSE; } /* success */ return TRUE; } static GUnixInputStream * passim_client_input_stream_from_bytes(GBytes *bytes, GError **error) { gint fd; gssize rc; #ifndef HAVE_MEMFD_CREATE gchar tmp_file[] = "/tmp/passim.XXXXXX"; #endif #ifdef HAVE_MEMFD_CREATE fd = memfd_create("passim", MFD_CLOEXEC); #else /* emulate in-memory file by an unlinked temporary file */ fd = g_mkstemp(tmp_file); if (fd != -1) { rc = g_unlink(tmp_file); if (rc != 0) { if (!g_close(fd, error)) { g_prefix_error(error, "failed to close temporary file: "); return NULL; } g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to unlink temporary file"); return NULL; } } #endif if (fd < 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to create memfd"); return NULL; } rc = write(fd, g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes)); if (rc < 0) { g_close(fd, NULL); g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to write %" G_GSSIZE_FORMAT, rc); return NULL; } if (lseek(fd, 0, SEEK_SET) < 0) { g_close(fd, NULL); g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to seek: %s", g_strerror(errno)); return NULL; } return G_UNIX_INPUT_STREAM(g_unix_input_stream_new(fd, TRUE)); } static GUnixInputStream * passim_client_input_stream_from_filename(const gchar *fn, GError **error) { gint fd = open(fn, O_RDONLY); if (fd < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed to open %s", fn); return NULL; } return G_UNIX_INPUT_STREAM(g_unix_input_stream_new(fd, TRUE)); } /** * passim_client_publish: * @self: a #PassimClient * @item: (not nullable): a #PassimItem * @error: (nullable): optional return location for an error * * Connects to the remote server. * * Returns: %TRUE for success * * Since: 0.1.0 **/ gboolean passim_client_publish(PassimClient *self, PassimItem *item, GError **error) { PassimClientPrivate *priv = GET_PRIVATE(self); g_autoptr(GDBusMessage) reply = NULL; g_autoptr(GDBusMessage) request = NULL; g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new(); g_autoptr(GUnixInputStream) istream = NULL; g_return_val_if_fail(PASSIM_IS_CLIENT(self), FALSE); g_return_val_if_fail(PASSIM_IS_ITEM(item), FALSE); g_return_val_if_fail(priv->proxy != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* set out of band file descriptor */ if (passim_item_get_stream(item) != NULL) { istream = g_object_ref(G_UNIX_INPUT_STREAM(passim_item_get_stream(item))); } else if (passim_item_get_file(item) != NULL) { g_autofree gchar *filename = g_file_get_path(passim_item_get_file(item)); istream = passim_client_input_stream_from_filename(filename, error); if (istream == NULL) return FALSE; } else if (passim_item_get_bytes(item) != NULL) { istream = passim_client_input_stream_from_bytes(passim_item_get_bytes(item), error); if (istream == NULL) return FALSE; } else { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "no PassimItem bytes or file set"); return FALSE; } g_unix_fd_list_append(fd_list, g_unix_input_stream_get_fd(istream), NULL); request = g_dbus_message_new_method_call(g_dbus_proxy_get_name(priv->proxy), g_dbus_proxy_get_object_path(priv->proxy), g_dbus_proxy_get_interface_name(priv->proxy), "Publish"); g_dbus_message_set_unix_fd_list(request, fd_list); /* call into daemon */ g_dbus_message_set_body(request, g_variant_new("(h@a{sv})", g_unix_input_stream_get_fd(istream), passim_item_to_variant(item))); reply = g_dbus_connection_send_message_with_reply_sync(g_dbus_proxy_get_connection(priv->proxy), request, G_DBUS_SEND_MESSAGE_FLAGS_NONE, G_MAXINT, NULL, NULL, /* cancellable */ error); if (reply == NULL) { if (error != NULL) g_dbus_error_strip_remote_error(*error); return FALSE; } if (g_dbus_message_to_gerror(reply, error)) return FALSE; /* success */ return TRUE; } static void passim_client_init(PassimClient *self) { } static void passim_client_finalize(GObject *object) { PassimClient *self = PASSIM_CLIENT(object); PassimClientPrivate *priv = GET_PRIVATE(self); if (priv->proxy != NULL) g_object_unref(priv->proxy); g_free(priv->version); g_free(priv->name); g_free(priv->uri); G_OBJECT_CLASS(passim_client_parent_class)->finalize(object); } static void passim_client_class_init(PassimClientClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = passim_client_finalize; } /** * passim_client_new: * * Creates a new client. * * Returns: a new #PassimClient * * Since: 0.1.0 **/ PassimClient * passim_client_new(void) { PassimClient *self; self = g_object_new(PASSIM_TYPE_CLIENT, NULL); return PASSIM_CLIENT(self); } passim-0.1.10/libpassim/passim-client.h000066400000000000000000000031601500514526500177740ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "passim-item.h" G_BEGIN_DECLS #define PASSIM_TYPE_CLIENT (passim_client_get_type()) G_DECLARE_DERIVABLE_TYPE(PassimClient, passim_client, PASSIM, CLIENT, GObject) struct _PassimClientClass { GObjectClass parent_class; /*< private >*/ void (*_passim_reserved1)(void); void (*_passim_reserved2)(void); void (*_passim_reserved3)(void); void (*_passim_reserved4)(void); void (*_passim_reserved5)(void); void (*_passim_reserved6)(void); void (*_passim_reserved7)(void); }; #define PASSIM_DBUS_SERVICE "org.freedesktop.Passim" #define PASSIM_DBUS_INTERFACE "org.freedesktop.Passim" #define PASSIM_DBUS_PATH "/" typedef enum { PASSIM_STATUS_UNKNOWN, PASSIM_STATUS_STARTING, PASSIM_STATUS_LOADING, PASSIM_STATUS_RUNNING, PASSIM_STATUS_DISABLED_METERED, } PassimStatus; PassimClient * passim_client_new(void); const gchar * passim_client_get_version(PassimClient *self); const gchar * passim_client_get_name(PassimClient *self); const gchar * passim_client_get_uri(PassimClient *self); PassimStatus passim_client_get_status(PassimClient *self); guint64 passim_client_get_download_saving(PassimClient *self); gdouble passim_client_get_carbon_saving(PassimClient *self); gboolean passim_client_load(PassimClient *self, GError **error); GPtrArray * passim_client_get_items(PassimClient *self, GError **error); gboolean passim_client_publish(PassimClient *self, PassimItem *item, GError **error); gboolean passim_client_unpublish(PassimClient *self, const gchar *hash, GError **error); G_END_DECLS passim-0.1.10/libpassim/passim-item.c000066400000000000000000000507401500514526500174550ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "passim-item.h" /** * PassimItem: * * A shared item. */ typedef struct { gchar *hash; PassimItemFlags flags; gchar *basename; gchar *cmdline; guint32 max_age; guint32 share_limit; guint32 share_count; guint64 size; GFile *file; GBytes *bytes; GInputStream *stream; GDateTime *ctime; } PassimItemPrivate; G_DEFINE_TYPE_WITH_PRIVATE(PassimItem, passim_item, G_TYPE_OBJECT) #define GET_PRIVATE(o) (passim_item_get_instance_private(o)) /** * passim_item_get_hash: * @self: a #PassimItem * * Gets the file hash. * * Returns: the typically in SHA-256 lowercase form, or %NULL if unset * * Since: 0.1.0 **/ const gchar * passim_item_get_hash(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->hash; } /** * passim_item_set_hash: * @self: a #PassimItem * @hash: (nullable): the hash, typically in SHA-256 lowercase form * * Sets the file hash. * * Since: 0.1.0 **/ void passim_item_set_hash(PassimItem *self, const gchar *hash) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* not changed */ if (g_strcmp0(priv->hash, hash) == 0) return; g_free(priv->hash); priv->hash = g_strdup(hash); } /** * passim_item_get_basename: * @self: a #PassimItem * * Gets the basename of the file that was published. * * Returns: the test basename, or %NULL if unset * * Since: 0.1.0 **/ const gchar * passim_item_get_basename(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->basename; } /** * passim_item_set_basename: * @self: a #PassimItem * @basename: (nullable): the basename name * * Sets the basename of the file that was published. * * Since: 0.1.0 **/ void passim_item_set_basename(PassimItem *self, const gchar *basename) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* not changed */ if (g_strcmp0(priv->basename, basename) == 0) return; g_free(priv->basename); priv->basename = g_strdup(basename); } /** * passim_item_get_cmdline: * @self: a #PassimItem * * Gets the cmdline of the binary that published the item. * * Returns: the binary name, or %NULL if unset * * Since: 0.1.0 **/ const gchar * passim_item_get_cmdline(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->cmdline; } /** * passim_item_set_cmdline: * @self: a #PassimItem * @cmdline: (nullable): the binary name * * Sets the cmdline of the binary that published the item. * * NOTE: this is desgined as a hint, and should not be used for security. The command line is * obtained via /proc//cmdline and it may be possible to falsify the data by a malicious * binary running as root under certain conditions. * * Since: 0.1.0 **/ void passim_item_set_cmdline(PassimItem *self, const gchar *cmdline) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* not changed */ if (g_strcmp0(priv->cmdline, cmdline) == 0) return; g_free(priv->cmdline); priv->cmdline = g_strdup(cmdline); } /** * passim_item_get_age: * @self: a #PassimItem * * Gets the current file age. * * Returns: time in seconds, or 0 for invalid. * * Since: 0.1.0 **/ guint32 passim_item_get_age(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_autoptr(GDateTime) dt_now = g_date_time_new_now_utc(); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); if (priv->ctime == NULL) return 0; return g_date_time_difference(dt_now, priv->ctime) / G_TIME_SPAN_SECOND; } /** * passim_item_get_max_age: * @self: a #PassimItem * * Gets the maximum permitted file age. * * Returns: time in seconds * * Since: 0.1.0 **/ guint32 passim_item_get_max_age(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); return priv->max_age; } /** * passim_item_set_max_age: * @self: a #PassimItem * @max_age: time in seconds * * Sets the maximum permitted file age. * * Since: 0.1.0 **/ void passim_item_set_max_age(PassimItem *self, guint32 max_age) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); priv->max_age = max_age; } /** * passim_item_get_share_limit: * @self: a #PassimItem * * Gets the maximum number of times that the file can be shared. * * Returns: share limit, or 0 if unset * * Since: 0.1.0 **/ guint32 passim_item_get_share_limit(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); return priv->share_limit; } /** * passim_item_set_share_limit: * @self: a #PassimItem * @share_limit: the share limit, or 0 * * Sets the maximum number of times that the file can be shared. * * Since: 0.1.0 **/ void passim_item_set_share_limit(PassimItem *self, guint32 share_limit) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); priv->share_limit = share_limit; } /** * passim_item_get_share_count: * @self: a #PassimItem * * Gets the current number of times the item has been shared to other machines. * * Returns: the count, or 0 if unset * * Since: 0.1.0 **/ guint32 passim_item_get_share_count(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); return priv->share_count; } /** * passim_item_set_share_count: * @self: a #PassimItem * @share_count: the count, or 0 to unset * * Sets the current number of times the item has been shared to other machines. * * Since: 0.1.0 **/ void passim_item_set_share_count(PassimItem *self, guint32 share_count) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); priv->share_count = share_count; } /** * passim_item_get_size: * @self: a #PassimItem * * Gets the size of the file in bytes. * * Returns: share limit, or 0 if unset * * Since: 0.1.2 **/ guint64 passim_item_get_size(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); return priv->size; } /** * passim_item_set_size: * @self: a #PassimItem * @size: the share limit, or 0 * * Sets the size of the file in bytes. * * Since: 0.1.2 **/ void passim_item_set_size(PassimItem *self, guint64 size) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); priv->size = size; } /** * passim_item_get_file: * @self: a #PassimItem * * Gets the local file in the cache. * * Returns: (transfer none): a #GFile, or %NULL if unset * * Since: 0.1.0 **/ GFile * passim_item_get_file(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->file; } /** * passim_item_set_file: * @self: a #PassimItem * @file: (nullable): a #GFile * * Sets the local file in the cache. * * Since: 0.1.0 **/ void passim_item_set_file(PassimItem *self, GFile *file) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* if not already set */ if (file != NULL && priv->basename == NULL) priv->basename = g_file_get_basename(file); g_set_object(&priv->file, file); } /** * passim_item_get_bytes: * @self: a #PassimItem * * Gets the local bytes in the cache. * * Returns: (transfer none): a #GBytes, or %NULL if unset * * Since: 0.1.0 **/ GBytes * passim_item_get_bytes(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->bytes; } /** * passim_item_set_bytes: * @self: a #PassimItem * @bytes: (nullable): a #GBytes * * Sets the local bytes in the cache. * * Since: 0.1.0 **/ void passim_item_set_bytes(PassimItem *self, GBytes *bytes) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* unchanged */ if (bytes == priv->bytes) return; if (priv->bytes != NULL) { g_bytes_unref(priv->bytes); priv->bytes = NULL; } if (bytes != NULL) { priv->bytes = g_bytes_ref(bytes); priv->size = g_bytes_get_size(bytes); } /* generate checksum */ if (bytes != NULL && priv->hash == NULL) priv->hash = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, bytes); } /** * passim_item_get_stream: * @self: a #PassimItem * * Gets the input stream for the item. * * Returns: (transfer none): a #GInputStream, or %NULL if unset * * Since: 0.1.5 **/ GInputStream * passim_item_get_stream(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->stream; } /** * passim_item_set_stream: * @self: a #PassimItem * @stream: (nullable): a #GInputStream * * Sets the input stream stream for the item. * * NOTE: This *MUST* be a #GUnixInputStream, or subclass thereof. * * Since: 0.1.5 **/ void passim_item_set_stream(PassimItem *self, GInputStream *stream) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); g_return_if_fail(G_IS_UNIX_INPUT_STREAM(stream)); g_set_object(&priv->stream, stream); } /** * passim_item_get_ctime: * @self: a #PassimItem * * Gets the creation time of the file. * * Returns: (transfer none): the creation time, or %NULL if unset * * Since: 0.1.0 **/ GDateTime * passim_item_get_ctime(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); return priv->ctime; } /** * passim_item_set_ctime: * @self: a #PassimItem * @ctime: (nullable): a #GDateTime * * Sets the creation time of the file. * * Since: 0.1.0 **/ void passim_item_set_ctime(PassimItem *self, GDateTime *ctime) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); /* not changed */ if (priv->ctime == ctime) return; if (priv->ctime != NULL) { g_date_time_unref(priv->ctime); priv->ctime = NULL; } if (ctime != NULL) priv->ctime = g_date_time_ref(ctime); } #if !GLIB_CHECK_VERSION(2, 70, 0) static GDateTime * g_file_info_get_creation_date_time(GFileInfo *info) { guint64 ctime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_CREATED); return g_date_time_new_from_unix_utc(ctime); } #endif /** * passim_item_load_filename: * @self: a #PassimItem * @filename: (not nullable): a filename with full path * @error: (nullable): optional return location for an error * * Loads the item from a file on disk. * * Since: 0.1.0 **/ gboolean passim_item_load_filename(PassimItem *self, const gchar *filename, GError **error) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), FALSE); g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); /* set file and bytes (which also sets the hash too) */ if (priv->file == NULL) { g_autoptr(GFile) file = g_file_new_for_path(filename); passim_item_set_file(self, file); } if (priv->bytes == NULL) { g_autoptr(GBytes) bytes = g_file_load_bytes(priv->file, NULL, NULL, error); if (bytes == NULL) return FALSE; passim_item_set_bytes(self, bytes); } if (priv->ctime == NULL) { g_autoptr(GFileInfo) info = g_file_query_info(priv->file, G_FILE_ATTRIBUTE_TIME_CREATED, G_FILE_QUERY_INFO_NONE, NULL, error); if (info == NULL) return FALSE; priv->ctime = g_file_info_get_creation_date_time(info); } if (priv->basename == NULL) priv->basename = g_file_get_basename(priv->file); /* success */ return TRUE; } /** * passim_item_to_variant: * @self: a #PassimItem * * Serialize the item data. * * Returns: the serialized data, or %NULL for error * * Since: 0.1.0 **/ GVariant * passim_item_to_variant(PassimItem *self) { GVariantBuilder builder; PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); if (priv->hash != NULL) g_variant_builder_add(&builder, "{sv}", "hash", g_variant_new_string(priv->hash)); if (priv->basename != NULL) { g_variant_builder_add(&builder, "{sv}", "filename", g_variant_new_string(priv->basename)); } if (priv->cmdline != NULL) { g_variant_builder_add(&builder, "{sv}", "cmdline", g_variant_new_string(priv->cmdline)); } if (priv->max_age != 0) { g_variant_builder_add(&builder, "{sv}", "max-age", g_variant_new_uint32(priv->max_age)); } if (priv->flags != PASSIM_ITEM_FLAG_NONE) { g_variant_builder_add(&builder, "{sv}", "flags", g_variant_new_uint64(priv->flags)); } if (priv->share_limit != 0) { g_variant_builder_add(&builder, "{sv}", "share-limit", g_variant_new_uint32(priv->share_limit)); } if (priv->size != 0) g_variant_builder_add(&builder, "{sv}", "size", g_variant_new_uint64(priv->size)); if (priv->share_count != 0) { g_variant_builder_add(&builder, "{sv}", "share-count", g_variant_new_uint32(priv->share_count)); } if (priv->ctime != NULL) { g_variant_builder_add(&builder, "{sv}", "ctime", g_variant_new_int64(g_date_time_to_unix(priv->ctime))); } return g_variant_builder_end(&builder); } /** * passim_item_from_variant: * @value: (not nullable): the serialized data * * Creates a new item using serialized data. * * Returns: (transfer full): a new #PassimItem, or %NULL if @value was invalid * * Since: 0.1.0 **/ PassimItem * passim_item_from_variant(GVariant *variant) { GVariant *value; const gchar *key; g_autoptr(GVariantIter) iter = NULL; g_autoptr(PassimItem) self = passim_item_new(); PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(variant != NULL, NULL); g_variant_get(variant, "a{sv}", &iter); while (g_variant_iter_next(iter, "{&sv}", &key, &value)) { if (g_strcmp0(key, "filename") == 0) priv->basename = g_variant_dup_string(value, NULL); if (g_strcmp0(key, "cmdline") == 0) priv->cmdline = g_variant_dup_string(value, NULL); if (g_strcmp0(key, "hash") == 0) priv->hash = g_variant_dup_string(value, NULL); if (g_strcmp0(key, "max-age") == 0) priv->max_age = g_variant_get_uint32(value); if (g_strcmp0(key, "share-limit") == 0) priv->share_limit = g_variant_get_uint32(value); if (g_strcmp0(key, "size") == 0) priv->size = g_variant_get_uint64(value); if (g_strcmp0(key, "share-count") == 0) priv->share_count = g_variant_get_uint32(value); if (g_strcmp0(key, "flags") == 0) priv->flags = g_variant_get_uint64(value); if (g_strcmp0(key, "ctime") == 0) { g_autoptr(GDateTime) dt = g_date_time_new_from_unix_utc(g_variant_get_int64(value)); passim_item_set_ctime(self, dt); } g_variant_unref(value); } return g_steal_pointer(&self); } /** * passim_item_get_flags: * @self: a #PassimItem * * Gets the item flags. * * Returns: item flags, or 0 if unset * * Since: 0.1.0 **/ guint64 passim_item_get_flags(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), 0); return priv->flags; } /** * passim_item_get_flags_as_string: * @self: a #PassimItem * * Gets the item flags. * * Returns: string * * Since: 0.1.0 **/ gchar * passim_item_get_flags_as_string(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); g_autoptr(GString) str = g_string_new(NULL); g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); for (guint i = 0; i < 64; i++) { if ((priv->flags & ((guint64)1 << i)) == 0) continue; if (str->len > 0) g_string_append(str, ","); g_string_append(str, passim_item_flag_to_string((guint64)1 << i)); } if (str->len == 0) g_string_append(str, passim_item_flag_to_string(PASSIM_ITEM_FLAG_NONE)); return g_string_free(g_steal_pointer(&str), FALSE); } /** * passim_item_set_flags: * @self: a #PassimItem * @flags: item flags, e.g. %PASSIM_ITEM_FLAG_NEXT_REBOOT * * Sets the item flags. * * Since: 0.1.0 **/ void passim_item_set_flags(PassimItem *self, guint64 flags) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); if (priv->flags == flags) return; priv->flags = flags; } /** * passim_item_add_flag: * @self: a #PassimItem * @flag: the #PassimItemFlags * * Adds a specific item flag to the item. * * Since: 0.1.0 **/ void passim_item_add_flag(PassimItem *self, PassimItemFlags flag) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); if (flag == 0) return; if ((priv->flags & flag) > 0) return; priv->flags |= flag; } /** * passim_item_remove_flag: * @self: a #PassimItem * @flag: a item flag * * Removes a specific item flag from the item. * * Since: 0.1.0 **/ void passim_item_remove_flag(PassimItem *self, PassimItemFlags flag) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_if_fail(PASSIM_IS_ITEM(self)); if (flag == 0) return; if ((priv->flags & flag) == 0) return; priv->flags &= ~flag; } /** * passim_item_has_flag: * @self: a #PassimItem * @flag: a item flag * * Finds if the item has a specific item flag. * * Returns: %TRUE if the flag is set * * Since: 0.1.0 **/ gboolean passim_item_has_flag(PassimItem *self, PassimItemFlags flag) { PassimItemPrivate *priv = GET_PRIVATE(self); g_return_val_if_fail(PASSIM_IS_ITEM(self), FALSE); return (priv->flags & flag) > 0; } /** * passim_item_flag_to_string: * @item_flag: item flags, e.g. %PASSIM_ITEM_FLAG_NEXT_REBOOT * * Converts an enumerated item flag to a string. * * Returns: identifier string * * Since: 0.1.0 **/ const gchar * passim_item_flag_to_string(PassimItemFlags item_flag) { if (item_flag == PASSIM_ITEM_FLAG_NONE) return "none"; if (item_flag == PASSIM_ITEM_FLAG_DISABLED) return "disabled"; if (item_flag == PASSIM_ITEM_FLAG_NEXT_REBOOT) return "next-reboot"; return NULL; } /** * passim_item_flag_from_string: * @item_flag: (nullable): a string, e.g. `next-reboot` * * Converts a string to an enumerated item flag. * * Returns: enumerated value * * Since: 0.1.0 **/ PassimItemFlags passim_item_flag_from_string(const gchar *item_flag) { if (g_strcmp0(item_flag, "none") == 0) return PASSIM_ITEM_FLAG_NONE; if (g_strcmp0(item_flag, "disabled") == 0) return PASSIM_ITEM_FLAG_DISABLED; if (g_strcmp0(item_flag, "next-reboot") == 0) return PASSIM_ITEM_FLAG_NEXT_REBOOT; return PASSIM_ITEM_FLAG_UNKNOWN; } /** * passim_item_to_string: * @self: a #PassimItem * * Builds a text representation of the object. * * Returns: text, or %NULL for invalid * * Since: 0.1.0 **/ gchar * passim_item_to_string(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); GString *str; g_return_val_if_fail(PASSIM_IS_ITEM(self), NULL); str = g_string_new(priv->hash); g_string_append_printf(str, " %s", priv->basename); if (priv->flags != PASSIM_ITEM_FLAG_NONE) { g_autofree gchar *flags = passim_item_get_flags_as_string(self); g_string_append_printf(str, " flags:%s", flags); } if (priv->cmdline != NULL) g_string_append_printf(str, " cmdline:%s", priv->cmdline); if (priv->max_age != G_MAXUINT32) g_string_append_printf(str, " age:%u/%u", passim_item_get_age(self), priv->max_age); if (priv->share_limit != G_MAXUINT32) g_string_append_printf(str, " share:%u/%u", priv->share_count, priv->share_limit); if (priv->size != 0) { g_autofree gchar *size = g_format_size(priv->size); g_string_append_printf(str, " size:%s", size); } return g_string_free(str, FALSE); } static void passim_item_init(PassimItem *self) { PassimItemPrivate *priv = GET_PRIVATE(self); priv->max_age = 24 * 60 * 60; priv->share_limit = 5; } static void passim_item_finalize(GObject *object) { PassimItem *self = PASSIM_ITEM(object); PassimItemPrivate *priv = GET_PRIVATE(self); if (priv->file != NULL) g_object_unref(priv->file); if (priv->bytes != NULL) g_bytes_unref(priv->bytes); if (priv->stream != NULL) g_object_unref(priv->stream); if (priv->ctime != NULL) g_date_time_unref(priv->ctime); g_free(priv->hash); g_free(priv->basename); g_free(priv->cmdline); G_OBJECT_CLASS(passim_item_parent_class)->finalize(object); } static void passim_item_class_init(PassimItemClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = passim_item_finalize; } /** * passim_item_new: * * Creates a new item. * * Returns: a new #PassimItem * * Since: 0.1.0 **/ PassimItem * passim_item_new(void) { PassimItem *self; self = g_object_new(PASSIM_TYPE_ITEM, NULL); return PASSIM_ITEM(self); } passim-0.1.10/libpassim/passim-item.h000066400000000000000000000067221500514526500174630ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include G_BEGIN_DECLS #define PASSIM_TYPE_ITEM (passim_item_get_type()) G_DECLARE_DERIVABLE_TYPE(PassimItem, passim_item, PASSIM, ITEM, GObject) struct _PassimItemClass { GObjectClass parent_class; /*< private >*/ void (*_passim_reserved1)(void); void (*_passim_reserved2)(void); void (*_passim_reserved3)(void); void (*_passim_reserved4)(void); void (*_passim_reserved5)(void); void (*_passim_reserved6)(void); void (*_passim_reserved7)(void); }; /** * PASSIM_ITEM_FLAG_NONE: * * No item flags are set. * * Since: 0.1.0 */ #define PASSIM_ITEM_FLAG_NONE 0u /** * PASSIM_ITEM_FLAG_DISABLED: * * The item is not active for some reason. * * Since: 0.1.0 */ #define PASSIM_ITEM_FLAG_DISABLED (1llu << 0) /** * PASSIM_ITEM_FLAG_NEXT_REBOOT: * * Only register the item when the machine has been rebooted. * * Since: 0.1.0 */ #define PASSIM_ITEM_FLAG_NEXT_REBOOT (1llu << 1) /** * PASSIM_ITEM_FLAG_UNKNOWN: * * The item flag is unknown. * * This is usually caused by a mismatched libpassimplugin and daemon. * * Since: 0.1.0 */ #define PASSIM_ITEM_FLAG_UNKNOWN G_MAXUINT64 /** * PassimItemFlags: * * Flags used to represent item attributes */ typedef guint64 PassimItemFlags; PassimItem * passim_item_new(void); gchar * passim_item_to_string(PassimItem *self); const gchar * passim_item_get_hash(PassimItem *self); void passim_item_set_hash(PassimItem *self, const gchar *hash); const gchar * passim_item_get_basename(PassimItem *self); void passim_item_set_basename(PassimItem *self, const gchar *basename); const gchar * passim_item_get_cmdline(PassimItem *self); void passim_item_set_cmdline(PassimItem *self, const gchar *cmdline); guint32 passim_item_get_age(PassimItem *self); guint32 passim_item_get_max_age(PassimItem *self); void passim_item_set_max_age(PassimItem *self, guint32 max_age); guint32 passim_item_get_share_limit(PassimItem *self); void passim_item_set_share_limit(PassimItem *self, guint32 share_limit); guint64 passim_item_get_size(PassimItem *self); void passim_item_set_size(PassimItem *self, guint64 size); guint32 passim_item_get_share_count(PassimItem *self); void passim_item_set_share_count(PassimItem *self, guint32 share_count); GFile * passim_item_get_file(PassimItem *self); void passim_item_set_file(PassimItem *self, GFile *file); GBytes * passim_item_get_bytes(PassimItem *self); void passim_item_set_bytes(PassimItem *self, GBytes *bytes); GInputStream * passim_item_get_stream(PassimItem *self); void passim_item_set_stream(PassimItem *self, GInputStream *stream); GDateTime * passim_item_get_ctime(PassimItem *self); void passim_item_set_ctime(PassimItem *self, GDateTime *ctime); guint64 passim_item_get_flags(PassimItem *self); gchar * passim_item_get_flags_as_string(PassimItem *self); void passim_item_set_flags(PassimItem *self, guint64 flags); void passim_item_add_flag(PassimItem *self, PassimItemFlags flag); void passim_item_remove_flag(PassimItem *self, PassimItemFlags flag); gboolean passim_item_has_flag(PassimItem *self, PassimItemFlags flag); const gchar * passim_item_flag_to_string(PassimItemFlags item_flag); PassimItemFlags passim_item_flag_from_string(const gchar *item_flag); gboolean passim_item_load_filename(PassimItem *self, const gchar *filename, GError **error); PassimItem * passim_item_from_variant(GVariant *value); GVariant * passim_item_to_variant(PassimItem *self); G_END_DECLS passim-0.1.10/libpassim/passim-version.c000066400000000000000000000011131500514526500201720ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-version.h" /** * passim_version_string: * * Gets the libpassim installed runtime version. * * This may be different to the *build-time* version if the daemon and library * objects somehow get out of sync. * * Returns: version string * * Since: 0.1.0 **/ const gchar * passim_version_string(void) { return G_STRINGIFY(PASSIM_MAJOR_VERSION) "." G_STRINGIFY( PASSIM_MINOR_VERSION) "." G_STRINGIFY(PASSIM_MICRO_VERSION); } passim-0.1.10/libpassim/passim-version.h.in000066400000000000000000000027021500514526500206110ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #if !defined(__PASSIM_H_INSIDE__) && !defined(PASSIM_COMPILATION) #error "Only can be included directly." #endif /* clang-format off */ /** * PASSIM_MAJOR_VERSION: * * The compile-time major version */ #define PASSIM_MAJOR_VERSION @MAJOR_VERSION@ /** * PASSIM_MINOR_VERSION: * * The compile-time minor version */ #define PASSIM_MINOR_VERSION @MINOR_VERSION@ /** * PASSIM_MICRO_VERSION: * * The compile-time micro version */ #define PASSIM_MICRO_VERSION @MICRO_VERSION@ /* clang-format on */ /** * PASSIM_CHECK_VERSION: * @major: Major version number * @minor: Minor version number * @micro: Micro version number * * Check whether a passim version equal to or greater than * major.minor.micro. * * These compile time macros allow the user to enable parts of client code * depending on the version of libpassim installed. */ #define PASSIM_CHECK_VERSION(major, minor, micro) \ (PASSIM_MAJOR_VERSION > major || \ (PASSIM_MAJOR_VERSION == major && PASSIM_MINOR_VERSION > minor) || \ (PASSIM_MAJOR_VERSION == major && PASSIM_MINOR_VERSION == minor && \ PASSIM_MICRO_VERSION >= micro)) const gchar * passim_version_string(void); passim-0.1.10/libpassim/passim.h000066400000000000000000000004071500514526500165210ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #define __PASSIM_H_INSIDE__ #include #include #include #undef __PASSIM_H_INSIDE__ passim-0.1.10/libpassim/passim.map000066400000000000000000000032411500514526500170460ustar00rootroot00000000000000# generated automatically, do not edit! LIBPASSIM_0.1.0 { global: passim_client_get_items; passim_client_get_type; passim_client_get_version; passim_client_load; passim_client_new; passim_client_publish; passim_client_unpublish; passim_item_add_flag; passim_item_flag_from_string; passim_item_flag_to_string; passim_item_from_variant; passim_item_get_age; passim_item_get_basename; passim_item_get_bytes; passim_item_get_cmdline; passim_item_get_ctime; passim_item_get_file; passim_item_get_flags; passim_item_get_flags_as_string; passim_item_get_hash; passim_item_get_max_age; passim_item_get_share_count; passim_item_get_share_limit; passim_item_get_type; passim_item_has_flag; passim_item_load_filename; passim_item_new; passim_item_remove_flag; passim_item_set_basename; passim_item_set_bytes; passim_item_set_cmdline; passim_item_set_ctime; passim_item_set_file; passim_item_set_flags; passim_item_set_hash; passim_item_set_max_age; passim_item_set_share_count; passim_item_set_share_limit; passim_item_to_string; passim_item_to_variant; passim_version_string; local: *; }; LIBPASSIM_0.1.2 { global: passim_client_get_status; passim_item_get_size; passim_item_set_size; local: *; } LIBPASSIM_0.1.0; LIBPASSIM_0.1.5 { global: passim_item_get_stream; passim_item_set_stream; local: *; } LIBPASSIM_0.1.2; LIBPASSIM_0.1.6 { global: passim_client_get_carbon_saving; passim_client_get_download_saving; passim_client_get_name; passim_client_get_uri; local: *; } LIBPASSIM_0.1.5; passim-0.1.10/meson.build000066400000000000000000000102221500514526500152270ustar00rootroot00000000000000project('passim', 'c', version: '0.1.10', license: 'LGPL-2.1-or-later', meson_version: '>=0.61.0', default_options: ['warning_level=2', 'c_std=c11'], ) # libtool versioning - this applies to libpassim libpassim_lt_current = '1' libpassim_lt_revision = '0' libpassim_lt_age = '0' libpassim_lt_version = '@0@.@1@.@2@'.format(libpassim_lt_current, libpassim_lt_age, libpassim_lt_revision) warning_flags = [ '-Waggregate-return', '-Wunused', '-Warray-bounds', '-Wcast-align', '-Wclobbered', '-Wdeclaration-after-statement', '-Wdiscarded-qualifiers', '-Wduplicated-branches', '-Wduplicated-cond', '-Wempty-body', '-Wformat=2', '-Wformat-nonliteral', '-Wformat-security', '-Wformat-signedness', '-Wignored-qualifiers', '-Wimplicit-function-declaration', '-Wimplicit-int', '-Winit-self', '-Wint-conversion', '-Wlogical-op', '-Wmaybe-uninitialized', '-Wmissing-declarations', '-Wmissing-format-attribute', '-Wmissing-include-dirs', '-Wmissing-noreturn', '-Wmissing-parameter-type', '-Wmissing-prototypes', '-Wnested-externs', '-Wno-cast-function-type', '-Wno-address-of-packed-member', # incompatible with g_autoptr() '-Wno-unknown-pragmas', '-Wno-missing-field-initializers', '-Wno-strict-aliasing', '-Wno-suggest-attribute=format', '-Wno-typedef-redefinition', '-Wno-unknown-warning-option', '-Wno-unused-parameter', '-Wold-style-definition', '-Woverride-init', '-Wpointer-arith', '-Wredundant-decls', '-Wreturn-type', '-Wshadow', '-Wsign-compare', '-Wstrict-aliasing', '-Wstrict-prototypes', '-Wswitch-default', '-Wtype-limits', '-Wundef', '-Wuninitialized', '-Wunused-but-set-variable', '-Wunused-variable', '-Wvla', '-Wwrite-strings' ] cc = meson.get_compiler('c') add_project_arguments(cc.get_supported_arguments(warning_flags), language: 'c') add_project_arguments('-DPASSIM_COMPILATION', language: 'c') # needed for memfd_create() add_project_arguments('-D_GNU_SOURCE', language: 'c') prefix = get_option('prefix') sysconfdir = prefix / get_option('sysconfdir') bindir = prefix / get_option('bindir') localstatedir = prefix / get_option('localstatedir') libexecdir = prefix / get_option('libexecdir') datadir = prefix / get_option('datadir') localedir = prefix / get_option('localedir') conf = configuration_data() conf.set_quoted('PACKAGE_NAME', meson.project_name()) conf.set_quoted('PACKAGE_DATADIR', datadir) conf.set_quoted('PACKAGE_SYSCONFDIR', sysconfdir) conf.set_quoted('PACKAGE_LOCALEDIR', localedir) conf.set_quoted('PACKAGE_LOCALSTATEDIR', localstatedir) conf.set_quoted('VERSION', meson.project_version()) conf.set_quoted('GETTEXT_PACKAGE', meson.project_name()) varr = meson.project_version().split('.') conf.set('MAJOR_VERSION', varr[0]) conf.set('MINOR_VERSION', varr[1]) conf.set('MICRO_VERSION', varr[2]) systemd = dependency('systemd', version: '>= 211') systemd_root_prefix = get_option('systemd_root_prefix') if systemd_root_prefix == '' pkgconfig_kwargs = {} else pkgconfig_kwargs = { 'pkgconfig_define': ['rootprefix', systemd_root_prefix], } endif systemdunitdir = systemd.get_variable(pkgconfig: 'systemdsystemunitdir', kwargs: pkgconfig_kwargs) sysusersdir = systemd.get_variable(pkgconfig: 'sysusersdir', kwargs: pkgconfig_kwargs) # get source version, falling back to package version git = find_program('git', required: false) tag = false if git.found() source_version = run_command([git, 'describe'], check: false).stdout().strip() if source_version == '' source_version = meson.project_version() endif tag = run_command([git, 'describe', '--exact-match'], check: false).returncode() == 0 else source_version = meson.project_version() endif conf.set_quoted('SOURCE_VERSION', source_version) root_incdir = include_directories('.') libgio = dependency('gio-unix-2.0', version: '>= 2.68.0') libsoup = dependency('libsoup-3.0', version: '>= 3.4.0') libgnutls = dependency('gnutls', version: '>= 3.6.0') if cc.has_function('memfd_create') conf.set('HAVE_MEMFD_CREATE', '1') endif configure_file( output: 'config.h', configuration: conf ) python3 = import('python').find_installation('python3') subdir('libpassim') subdir('src') subdir('data') subdir('po') passim-0.1.10/meson_options.txt000066400000000000000000000003421500514526500165240ustar00rootroot00000000000000option('systemd_root_prefix', type: 'string', value: '', description: 'Directory to base systemd’s installation directories on') option('introspection', type : 'feature', description : 'generate GObject Introspection data') passim-0.1.10/po/000077500000000000000000000000001500514526500135065ustar00rootroot00000000000000passim-0.1.10/po/LINGUAS000066400000000000000000000000441500514526500145310ustar00rootroot00000000000000en_GB nb_NO cs de fr tr ka ru ta fi passim-0.1.10/po/POTFILES.in000066400000000000000000000000211500514526500152540ustar00rootroot00000000000000src/passim-cli.c passim-0.1.10/po/cs.po000066400000000000000000000101161500514526500144520ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-03-19 15:36+0000\n" "PO-Revision-Date: 2024-03-22 19:02+0000\n" "Last-Translator: Matej Cepl \n" "Language-Team: Czech " "\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Weblate 5.5-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:179 #, fuzzy msgid "Disabled" msgstr "Zakázané" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:183 msgid "Next Reboot" msgstr "Příští restart" #. TRANSLATORS: item file basename #: src/passim-cli.c:196 msgid "Filename" msgstr "Jméno souboru" #. TRANSLATORS: item flags #: src/passim-cli.c:203 msgid "Flags" msgstr "Značky" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:210 msgid "Command Line" msgstr "Příkazová řádka" #. TRANSLATORS: age of the published item #: src/passim-cli.c:217 #, fuzzy msgid "Age" msgstr "Věk" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:226 #, fuzzy msgid "Share Limit" msgstr "Limit sdílení" #. TRANSLATORS: size of the published item #: src/passim-cli.c:235 #, fuzzy msgid "Size" msgstr "Velikost" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:267 #, fuzzy msgid "Loading…" msgstr "Nahrávání .." #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:270 #, fuzzy msgid "Disabled (metered network)" msgstr "Zakázané (měřená síť)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:273 msgid "Running" msgstr "Běží" #: src/passim-cli.c:277 #, fuzzy msgid "Status" msgstr "Stav" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:284 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:323 src/passim-cli.c:353 #, fuzzy msgid "Invalid arguments" msgstr "Neplatný argument" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:340 msgid "Published" msgstr "Zveřejňuje" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:360 msgid "Unpublished" msgstr "Nezveřejňuje" #. TRANSLATORS: --version #: src/passim-cli.c:375 #, fuzzy msgid "Show project version" msgstr "Zobrazit verzi projektu" #: src/passim-cli.c:382 #, fuzzy msgid "Next reboot" msgstr "Příští restart" #. TRANSLATORS: CLI action description #: src/passim-cli.c:397 #, fuzzy msgid "Show daemon status" msgstr "Zobrazit stav daemonu" #. TRANSLATORS: CLI option example #: src/passim-cli.c:402 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "JMÉNO-SOUBORU [NEJSTARŠÍ] [NEJDÉLE VEŘEJNÝ]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:404 #, fuzzy msgid "Publish an additional file" msgstr "Publikovat další soubor" #. TRANSLATORS: CLI option example #: src/passim-cli.c:409 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:411 msgid "Unpublish an existing file" msgstr "Nezveřejnit existující soubor" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:418 #, fuzzy msgid "Interact with the local passimd process." msgstr "Interakce s lokálním procesem." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:420 #, fuzzy msgid "Passim CLI" msgstr "Passim CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:424 msgid "Failed to parse arguments" msgstr "Analýza argumentů selhala" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:432 msgid "Failed to connect to daemon" msgstr "Nepodařilo se připojit k daemon" #. TRANSLATORS: CLI tool #: src/passim-cli.c:439 msgid "client version" msgstr "verze klientu" #. TRANSLATORS: server #: src/passim-cli.c:441 msgid "daemon version" msgstr "verze démonu" passim-0.1.10/po/de.po000066400000000000000000000100061500514526500144330ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-03-19 15:36+0000\n" "PO-Revision-Date: 2024-03-22 19:02+0000\n" "Last-Translator: Paul Lettich \n" "Language-Team: German " "\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.5-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:179 msgid "Disabled" msgstr "Deaktiviert" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:183 msgid "Next Reboot" msgstr "Nächster Neustart" #. TRANSLATORS: item file basename #: src/passim-cli.c:196 msgid "Filename" msgstr "Dateiname" #. TRANSLATORS: item flags #: src/passim-cli.c:203 msgid "Flags" msgstr "Optionen" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:210 msgid "Command Line" msgstr "Kommandozeile" #. TRANSLATORS: age of the published item #: src/passim-cli.c:217 msgid "Age" msgstr "Alter" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:226 msgid "Share Limit" msgstr "" #. TRANSLATORS: size of the published item #: src/passim-cli.c:235 msgid "Size" msgstr "Größe" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:267 msgid "Loading…" msgstr "Laden…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:270 #, fuzzy msgid "Disabled (metered network)" msgstr "Deaktiviert (getaktetes Netzwerk)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:273 msgid "Running" msgstr "" #: src/passim-cli.c:277 msgid "Status" msgstr "Status" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:284 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:323 src/passim-cli.c:353 msgid "Invalid arguments" msgstr "Ungültige Parameter" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:340 msgid "Published" msgstr "Veröffentlicht" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:360 msgid "Unpublished" msgstr "Veröffentlichung gestoppt" #. TRANSLATORS: --version #: src/passim-cli.c:375 msgid "Show project version" msgstr "Projektversion anzeigen" #: src/passim-cli.c:382 msgid "Next reboot" msgstr "Nächster Neustart" #. TRANSLATORS: CLI action description #: src/passim-cli.c:397 msgid "Show daemon status" msgstr "Status des Daemons anzeigen" #. TRANSLATORS: CLI option example #: src/passim-cli.c:402 #, fuzzy msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "DATEINAME [MAX-ALTER] [MAX-VERTEILUNG]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:404 msgid "Publish an additional file" msgstr "Eine weitere Datei veröffentlichen" #. TRANSLATORS: CLI option example #: src/passim-cli.c:409 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:411 msgid "Unpublish an existing file" msgstr "Veröffentlichung einer existierenden Datei stoppen" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:418 msgid "Interact with the local passimd process." msgstr "Mit dem lokalen passimd-Prozess interagieren." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:420 msgid "Passim CLI" msgstr "Passim CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:424 msgid "Failed to parse arguments" msgstr "Verarbeiten der Parameter fehlgeschlagen" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:432 msgid "Failed to connect to daemon" msgstr "Verbindung zum Daemon fehlgeschlagen" #. TRANSLATORS: CLI tool #: src/passim-cli.c:439 msgid "client version" msgstr "Version des Clients" #. TRANSLATORS: server #: src/passim-cli.c:441 msgid "daemon version" msgstr "Version des Daemons" passim-0.1.10/po/en_GB.po000066400000000000000000000076041500514526500150270ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-03-19 15:36+0000\n" "PO-Revision-Date: 2024-03-19 16:51+0000\n" "Last-Translator: Richard Hughes \n" "Language-Team: English (United Kingdom) \n" "Language: en_GB\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.5-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:179 msgid "Disabled" msgstr "Disabled" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:183 msgid "Next Reboot" msgstr "Next Reboot" #. TRANSLATORS: item file basename #: src/passim-cli.c:196 msgid "Filename" msgstr "Filename" #. TRANSLATORS: item flags #: src/passim-cli.c:203 msgid "Flags" msgstr "Flags" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:210 msgid "Command Line" msgstr "Command Line" #. TRANSLATORS: age of the published item #: src/passim-cli.c:217 msgid "Age" msgstr "Age" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:226 msgid "Share Limit" msgstr "Share Limit" #. TRANSLATORS: size of the published item #: src/passim-cli.c:235 msgid "Size" msgstr "Size" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:267 msgid "Loading…" msgstr "Loading…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:270 msgid "Disabled (metered network)" msgstr "Disabled (metered network)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:273 msgid "Running" msgstr "Running" #: src/passim-cli.c:277 msgid "Status" msgstr "Status" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:284 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:323 src/passim-cli.c:353 msgid "Invalid arguments" msgstr "Invalid arguments" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:340 msgid "Published" msgstr "Published" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:360 msgid "Unpublished" msgstr "Unpublished" #. TRANSLATORS: --version #: src/passim-cli.c:375 msgid "Show project version" msgstr "Show project version" #: src/passim-cli.c:382 msgid "Next reboot" msgstr "Next reboot" #. TRANSLATORS: CLI action description #: src/passim-cli.c:397 msgid "Show daemon status" msgstr "Show daemon status" #. TRANSLATORS: CLI option example #: src/passim-cli.c:402 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "FILENAME [MAX-AGE] [MAX-SHARE]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:404 msgid "Publish an additional file" msgstr "Publish an additional file" #. TRANSLATORS: CLI option example #: src/passim-cli.c:409 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:411 msgid "Unpublish an existing file" msgstr "Unpublish an existing file" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:418 msgid "Interact with the local passimd process." msgstr "Interact with the local passimd process." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:420 msgid "Passim CLI" msgstr "Passim CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:424 msgid "Failed to parse arguments" msgstr "Failed to parse arguments" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:432 msgid "Failed to connect to daemon" msgstr "Failed to connect to daemon" #. TRANSLATORS: CLI tool #: src/passim-cli.c:439 msgid "client version" msgstr "client version" #. TRANSLATORS: server #: src/passim-cli.c:441 msgid "daemon version" msgstr "daemon version" passim-0.1.10/po/fi.po000066400000000000000000000131141500514526500144440ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: 2025-03-02 11:02+0000\n" "Last-Translator: Ricky Tigg \n" "Language-Team: Finnish \n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.10.3-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "Poistettu käytöstä" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "Seuraava uudelleenkäynnistys" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "Tiedoston nimi" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "Liput" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "Komentorivi" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "Ikä" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "Jaa raja" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "Koko" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "Nimi" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "Ladataan…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "Poissa käytöstä (mitattu verkko)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "Käynnissä" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "Tila" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "Verkkosäästö" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "Hiilisäästö" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "Epäkelvolliset argumentit" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "Julkaistu" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "Julkaisematon" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "Epäkelvolliset argumentit; odotettu ALUSTANIMI HAJAUTUS" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "%s:n lataaminen %s:sta epäonnistui: %s" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "" "%s:n lataaminen %s:sta epäonnistui, koska tiedoston tarkistussumma ei täsmää" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "Säästetty %s (%s/%s)" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "Näytä projektin versio" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "Seuraava uudelleenkäynnistys" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "Näytä demonin tila" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "TIEDOSTON_NIMI [MAKS-IKÄ] [MAKS-JAKO]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "Julkaise lisätiedosto" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "HAJAUTUS" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "Peruuta olemassa olevan tiedoston julkaisu" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "ALUSTANIMI HAJAUTUS" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "Lataa tiedosto etäkoneelta" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "Ole vuorovaikutuksessa paikallisen passimd-prosessin kanssa." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "Passim-CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "Argumenttien jäsentäminen epäonnistui" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "Yhteyden muodostaminen demoniin epäonnistui" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "asiakasversio" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "demoniversio" passim-0.1.10/po/fix_translations.py000077500000000000000000000023331500514526500174530ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: LGPL-2.1-or-later import sys import os import subprocess def _do_msgattrib(fn): argv = [ "msgattrib", "--no-location", "--translated", "--no-wrap", "--sort-output", fn, "--output-file=" + fn, ] ret = subprocess.run(argv) if ret.returncode != 0: return def _do_nukeheader(fn): clean_lines = [] with open(fn) as f: lines = f.readlines() for line in lines: if line.startswith('"POT-Creation-Date:'): continue if line.startswith('"PO-Revision-Date:'): continue if line.startswith('"Last-Translator:'): continue clean_lines.append(line) with open(fn, "w") as f: f.writelines(clean_lines) def _process_file(fn): _do_msgattrib(fn) _do_nukeheader(fn) if __name__ == "__main__": if len(sys.argv) == 1: print("path required") sys.exit(1) try: dirname = sys.argv[1] for fn in os.listdir(dirname): if fn.endswith(".po"): _process_file(os.path.join(dirname, fn)) except NotADirectoryError: print("path required") sys.exit(2) passim-0.1.10/po/fr.po000066400000000000000000000077771500514526500144770ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-03-19 15:36+0000\n" "PO-Revision-Date: 2024-03-27 10:01+0000\n" "Last-Translator: Michael S \n" "Language-Team: French " "\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" "X-Generator: Weblate 5.5-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:179 msgid "Disabled" msgstr "Désactivé" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:183 msgid "Next Reboot" msgstr "Lors du prochain redémarrage" #. TRANSLATORS: item file basename #: src/passim-cli.c:196 msgid "Filename" msgstr "Nom du fichier" #. TRANSLATORS: item flags #: src/passim-cli.c:203 msgid "Flags" msgstr "Drapeaux" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:210 msgid "Command Line" msgstr "Ligne de commande" #. TRANSLATORS: age of the published item #: src/passim-cli.c:217 msgid "Age" msgstr "Âge" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:226 msgid "Share Limit" msgstr "Limite de partage" #. TRANSLATORS: size of the published item #: src/passim-cli.c:235 msgid "Size" msgstr "Taille" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:267 msgid "Loading…" msgstr "Chargement…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:270 msgid "Disabled (metered network)" msgstr "Désactivé (réseau limité)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:273 msgid "Running" msgstr "En cours d’exécution" #: src/passim-cli.c:277 msgid "Status" msgstr "Statut" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:284 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:323 src/passim-cli.c:353 msgid "Invalid arguments" msgstr "Arguments invalides" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:340 msgid "Published" msgstr "Publié" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:360 msgid "Unpublished" msgstr "Dépublié" #. TRANSLATORS: --version #: src/passim-cli.c:375 msgid "Show project version" msgstr "Affiche la version du projet" #: src/passim-cli.c:382 msgid "Next reboot" msgstr "Prochain redémarrage" #. TRANSLATORS: CLI action description #: src/passim-cli.c:397 msgid "Show daemon status" msgstr "Affiche le statut du daemon" #. TRANSLATORS: CLI option example #: src/passim-cli.c:402 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "NOM DU FICHIER [AGE MAXIMUM] [NOMBRE DE PARTAGE]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:404 msgid "Publish an additional file" msgstr "Publier un fichier additionnel" #. TRANSLATORS: CLI option example #: src/passim-cli.c:409 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:411 msgid "Unpublish an existing file" msgstr "Dépublier un fichier existant" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:418 msgid "Interact with the local passimd process." msgstr "Interagit avec le processus passimd local." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:420 msgid "Passim CLI" msgstr "Ligne de commande Passim" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:424 msgid "Failed to parse arguments" msgstr "Impossible d'analyser les arguments" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:432 msgid "Failed to connect to daemon" msgstr "Contact avec le daemon impossible" #. TRANSLATORS: CLI tool #: src/passim-cli.c:439 msgid "client version" msgstr "version du client" #. TRANSLATORS: server #: src/passim-cli.c:441 msgid "daemon version" msgstr "version du daemon" passim-0.1.10/po/ka.po000066400000000000000000000155521500514526500144510ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: 2024-08-10 04:09+0000\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian \n" "Language: ka\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.7-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "გამორთულია" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "შემდეგი გადატვირთვა" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "ფაილის სახელი" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "ალმები" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "ბრძანების ველი" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "ასაკი" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "გაზიარებების შეზღუდვა" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "ზომა" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "სახელი" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "ჩატვირთვა…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "გათიშულია (გაზომვადი ქსელი)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "გაშვებულია" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "სტატუსი" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "ქსელის დაზოგვა" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "გაზოგილი ნახშირბადი" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "არასწორი არგუმენტები" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "გამოქვეყნებულია" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "გამოუქვეყნებელი" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "არასწორი არგუმენტები. მოველოდი საბაზისოსახელი ჰეში" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "%s-ის გადმოწერა %s-დან ჩავარდა: %s" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "%s-ის გადმოწერა %s-დან ჩავარდა, რადგან საკონტროლო ჯამი არ ემთხვევა" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "შენახულია %s (%s %s-დან)" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "პროექტის ვერსიის ჩვენება" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "შემდეგი გადატვირთვის შემდეგ" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "დემონის სტატუსის ჩვენება" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "ფაილისსახელი [მაქს-ასაკი] [მაქს-გაზიარება]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "დამატებითი ფაილის გამოქვეყნება" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "ჰეში" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "არსებული ფაილის გამოქვეყნების მოხსნა" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "საბაზისოსახელი ჰეში" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "ფაილის გადმოწერა დაშორებული მანქანიდან" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "მიმართვა passimd-ის ლოკალურ პროცესზე." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "Passim CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "არგუმენტების დამუშავების შეცდომა" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "დემონთან მიერთება ჩავარდა" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "კლიენტის ვერსია" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "დემონის ვერსია" passim-0.1.10/po/meson.build000066400000000000000000000004331500514526500156500ustar00rootroot00000000000000i18n = import('i18n') i18n.gettext(meson.project_name(), preset: 'glib', args: [ '--default-domain=' + meson.project_name(), ] ) run_target('fix-translations', command: [ [python3, files('fix_translations.py')], join_paths(meson.project_source_root(), 'po') ] ) passim-0.1.10/po/nb_NO.po000066400000000000000000000076531500514526500150540ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-03-19 15:36+0000\n" "PO-Revision-Date: 2024-03-26 02:01+0000\n" "Last-Translator: Allan Nordhøy \n" "Language-Team: Norwegian Bokmål \n" "Language: nb_NO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.5-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:179 msgid "Disabled" msgstr "Deaktivert" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:183 msgid "Next Reboot" msgstr "Neste omstart" #. TRANSLATORS: item file basename #: src/passim-cli.c:196 msgid "Filename" msgstr "Filnavn" #. TRANSLATORS: item flags #: src/passim-cli.c:203 msgid "Flags" msgstr "Flagg" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:210 msgid "Command Line" msgstr "Kommandolinje" #. TRANSLATORS: age of the published item #: src/passim-cli.c:217 msgid "Age" msgstr "Alder" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:226 msgid "Share Limit" msgstr "Delingsgrense" #. TRANSLATORS: size of the published item #: src/passim-cli.c:235 msgid "Size" msgstr "Størrelse" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:267 msgid "Loading…" msgstr "Laster …" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:270 msgid "Disabled (metered network)" msgstr "Deaktivert (betalt dataforbindelse)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:273 msgid "Running" msgstr "Kjører" #: src/passim-cli.c:277 msgid "Status" msgstr "Status" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:284 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:323 src/passim-cli.c:353 msgid "Invalid arguments" msgstr "Ugyldige argumenter" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:340 msgid "Published" msgstr "Publisert" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:360 msgid "Unpublished" msgstr "Upublisert" #. TRANSLATORS: --version #: src/passim-cli.c:375 msgid "Show project version" msgstr "Vis prosjektversjon" #: src/passim-cli.c:382 msgid "Next reboot" msgstr "Neste omstart" #. TRANSLATORS: CLI action description #: src/passim-cli.c:397 msgid "Show daemon status" msgstr "Vis tjenestens status" #. TRANSLATORS: CLI option example #: src/passim-cli.c:402 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "FILNAVN [MAKS-ALDER] [MAX-DELING]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:404 msgid "Publish an additional file" msgstr "Publiser en fil i tillegg" #. TRANSLATORS: CLI option example #: src/passim-cli.c:409 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:411 msgid "Unpublish an existing file" msgstr "Av-publiser en eksisterende fil" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:418 msgid "Interact with the local passimd process." msgstr "Samhandle med den lokale passimd-prosessen." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:420 msgid "Passim CLI" msgstr "Passim-CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:424 msgid "Failed to parse arguments" msgstr "Kunne ikke tolke argumentene" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:432 msgid "Failed to connect to daemon" msgstr "FIkk ikke kontakt med tjenesten" #. TRANSLATORS: CLI tool #: src/passim-cli.c:439 msgid "client version" msgstr "klientversjon" #. TRANSLATORS: server #: src/passim-cli.c:441 msgid "daemon version" msgstr "nisse-versjon" passim-0.1.10/po/passim.pot000066400000000000000000000111501500514526500155240ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "" #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "" passim-0.1.10/po/ru.po000066400000000000000000000143641500514526500145040ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: 2024-06-28 16:09+0000\n" "Last-Translator: Airat Makhmutov \n" "Language-Team: Russian \n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" "X-Generator: Weblate 5.7-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "Выключен" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "Следующая Перезагрузка" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "Имя файла" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "Флаги" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "Командная строка" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "Возраст" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "Лимит Раздачи" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "Размер" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "Имя" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "Загрузка…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "Выключен (ограниченный интернет)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "Работает" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "Статус" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "Экономия Интернета" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "Экономия Углерода" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "Недопустимые аргументы" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "Опубликовано" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "Не опубликовано" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "Недопустимые аргументы, ожидалось BASENAME HASH" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "Не удалось скачать %s из %s:%s" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "" "Не удалось скачать %s из %s так как контрольная сумма файла не совпадает" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "Сохранено %s (%s %s)" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "Показать версию проекта" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "Следующая перезагрузка" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "Показать статус демона" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "ИМЯ-ФАЙЛА [МАКСИМАЛЬНЫЙ-ВОЗРАСТ] [МАКСИМАЛЬНАЯ-РАЗДАЧА]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "Опубликовать дополнительный файл" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "Отменить публикацию существующего файла" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "BASENAME HASH" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "Загрузите файл с удаленной машины" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "Взаимодействие с локальным процессом passimd." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "Passim CLI" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "Не удалось проанализировать аргументы" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "Не удалось подключиться к демону" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "версия клиента" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "версия демона" passim-0.1.10/po/ta.po000066400000000000000000000156351500514526500144640ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: 2025-01-27 13:13+0000\n" "Last-Translator: தமிழ்நேரம் \n" "Language-Team: Tamil \n" "Language: ta\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.10-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "முடக்கப்பட்டது" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "அடுத்த மறுதொடக்கம்" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "கோப்புப்பெயர்" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "கொடிகள்" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "கட்டளை வரி" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "அகவை" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "பகிர்வு வரம்பு" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "அளவு" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "பெயர்" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "ஏற்றுகிறது…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "முடக்கப்பட்ட (மீட்டர் நெட்வொர்க்)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "இயங்கும்" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "நிலை" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "பிணைய சேமிப்பு" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "கார்பன் சேமிப்பு" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "யூரி" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "தவறான வாதங்கள்" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "வெளியிடப்பட்டது" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "வெளியிடப்படாதது" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "தவறான வாதங்கள், எதிர்பார்க்கப்படும் பேசன் பெயர் ஆச்" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "%s: %s இலிருந்து %s ஐ பதிவிறக்கத் தவறிவிட்டது" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "கோப்பு செக்சம் பொருந்தாததால் %s இலிருந்து %s ஐ பதிவிறக்குவதில் தோல்வி" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "%s ( %s %s) சேமிக்கப்பட்டது" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "திட்ட பதிப்பைக் காட்டு" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "அடுத்த மறுதொடக்கம்" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "டீமான் நிலையைக் காட்டு" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "கோப்பு பெயர் [மேக்ச்-ஏச்] [மேக்ச்-சேர்]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "கூடுதல் கோப்பை வெளியிடுங்கள்" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "ஆச்" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "ஏற்கனவே உள்ள கோப்பை வெளியிடுங்கள்" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "பேசன்ம் ஆச்" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "தொலைநிலை இயந்திரத்திலிருந்து ஒரு கோப்பைப் பதிவிறக்கவும்" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "உள்ளக பாசிம்ட் செயல்முறையுடன் தொடர்பு கொள்ளுங்கள்." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "பாசிம் கிளி" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "வாதங்களை அலசத் தவறிவிட்டது" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "டீமனுடன் இணைக்கத் தவறிவிட்டது" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "கிளீன் பதிப்பு" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "டீமான் பதிப்பு" passim-0.1.10/po/tr.po000066400000000000000000000131221500514526500144720ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the passim package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: passim\n" "Report-Msgid-Bugs-To: richard@hughsie.com\n" "POT-Creation-Date: 2024-04-15 14:10+0100\n" "PO-Revision-Date: 2024-04-24 17:07+0000\n" "Last-Translator: Oğuz Ersen \n" "Language-Team: Turkish \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.5.1-dev\n" #. TRANSLATORS: the item is not enabled #: src/passim-cli.c:180 msgid "Disabled" msgstr "Devre dışı" #. TRANSLATORS: only begin sharing the item after the next restart #: src/passim-cli.c:184 msgid "Next Reboot" msgstr "Sonraki Yeniden Başlatmada" #. TRANSLATORS: item file basename #: src/passim-cli.c:197 msgid "Filename" msgstr "Dosya Adı" #. TRANSLATORS: item flags #: src/passim-cli.c:204 msgid "Flags" msgstr "Bayraklar" #. TRANSLATORS: basename of the thing that published the item #: src/passim-cli.c:211 msgid "Command Line" msgstr "Komut Satırı" #. TRANSLATORS: age of the published item #: src/passim-cli.c:218 msgid "Age" msgstr "Yaş" #. TRANSLATORS: number of times we can share the item #: src/passim-cli.c:227 msgid "Share Limit" msgstr "Paylaşım Sınırı" #. TRANSLATORS: size of the published item #: src/passim-cli.c:236 msgid "Size" msgstr "Boyut" #. TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" #: src/passim-cli.c:271 msgid "Name" msgstr "Ad" #. TRANSLATORS: daemon is starting up #: src/passim-cli.c:278 msgid "Loading…" msgstr "Yükleniyor…" #. TRANSLATORS: daemon is scared to publish files #: src/passim-cli.c:281 msgid "Disabled (metered network)" msgstr "Devre dışı (kotalı ağ)" #. TRANSLATORS: daemon is offering files like normal #: src/passim-cli.c:284 msgid "Running" msgstr "Çalışıyor" #. TRANSLATORS: what the daemon is doing right now, e.g. 'Running' #: src/passim-cli.c:289 msgid "Status" msgstr "Durum" #. TRANSLATORS: how many bytes we did not download from the internet #: src/passim-cli.c:297 msgid "Network Saving" msgstr "Ağ Tasarrufu" #. TRANSLATORS: how much carbon we did not *burn* by using local data #: src/passim-cli.c:304 msgid "Carbon Saving" msgstr "Karbon Tasarrufu" #. TRANSLATORS: full https://whatever of the daemon #: src/passim-cli.c:312 msgid "URI" msgstr "URI" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:351 src/passim-cli.c:381 msgid "Invalid arguments" msgstr "Geçersiz argümanlar" #. TRANSLATORS: now sharing to the world #: src/passim-cli.c:368 msgid "Published" msgstr "Yayınlanıyor" #. TRANSLATORS: no longer sharing with the world #: src/passim-cli.c:388 msgid "Unpublished" msgstr "Yayınlanmıyor" #. TRANSLATORS: user mistyped the command #: src/passim-cli.c:426 msgid "Invalid arguments, expected BASENAME HASH" msgstr "Geçersiz argümanlar, TEMEL-AD SAĞLAMA-TOPLAMI bekleniyor" #. TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is #. the untranslated error phrase #: src/passim-cli.c:469 #, c-format msgid "Failed to download %s from %s: %s" msgstr "%s indirilemedi (%s adresinden): %s" #. TRANSLATORS: %1 is a filename, %2 is a internet address #: src/passim-cli.c:483 #, c-format msgid "Failed to download %s from %s as file checksum does not match" msgstr "Dosya sağlama toplamı eşleşmediği için %s indirilemedi (%s adresinden)" #. TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. #. * 'application/zstd') #: src/passim-cli.c:502 #, c-format msgid "Saved %s (%s of %s)" msgstr "%s kaydedildi (%s - %s)" #. TRANSLATORS: --version #: src/passim-cli.c:518 msgid "Show project version" msgstr "Proje sürümünü göster" #: src/passim-cli.c:525 msgid "Next reboot" msgstr "Sonraki yeniden başlatmada" #. TRANSLATORS: CLI action description #: src/passim-cli.c:540 msgid "Show daemon status" msgstr "Sunucu durumunu göster" #. TRANSLATORS: CLI option example #: src/passim-cli.c:545 msgid "FILENAME [MAX-AGE] [MAX-SHARE]" msgstr "DOSYA-ADI [AZAMİ-YAŞ] [AZAMİ-PAYLAŞIM]" #. TRANSLATORS: CLI action description #: src/passim-cli.c:547 msgid "Publish an additional file" msgstr "Ek bir dosya yayınlayın" #. TRANSLATORS: CLI option example #: src/passim-cli.c:552 msgid "HASH" msgstr "SAĞLAMA-TOPLAMI" #. TRANSLATORS: CLI action description #: src/passim-cli.c:554 msgid "Unpublish an existing file" msgstr "Var olan bir dosyayı yayından kaldırın" #. TRANSLATORS: CLI option example #: src/passim-cli.c:559 msgid "BASENAME HASH" msgstr "TEMEL-AD SAĞLAMA-TOPLAMI" #. TRANSLATORS: CLI action description #: src/passim-cli.c:561 msgid "Download a file from a remote machine" msgstr "Uzak makineden bir dosya indirin" #. TRANSLATORS: CLI tool description #: src/passim-cli.c:568 msgid "Interact with the local passimd process." msgstr "Yerel passimd programıyla etkileşim kurun." #. TRANSLATORS: CLI tool name #: src/passim-cli.c:570 msgid "Passim CLI" msgstr "Passim komut satırı arayüzü" #. TRANSLATORS: we don't know what to do #: src/passim-cli.c:574 msgid "Failed to parse arguments" msgstr "Argümanlar ayrıştırılamadı" #. TRANSLATORS: daemon failed to start #: src/passim-cli.c:582 msgid "Failed to connect to daemon" msgstr "Sunucuya bağlanılamadı" #. TRANSLATORS: CLI tool #: src/passim-cli.c:589 msgid "client version" msgstr "istemci sürümü" #. TRANSLATORS: server #: src/passim-cli.c:591 msgid "daemon version" msgstr "sunucu sürümü" passim-0.1.10/src/000077500000000000000000000000001500514526500136575ustar00rootroot00000000000000passim-0.1.10/src/meson.build000066400000000000000000000026461500514526500160310ustar00rootroot00000000000000install_data( 'org.freedesktop.Passim.xml', install_dir: datadir / 'dbus-1/interfaces', ) install_data( 'passim.1', install_dir: datadir / 'man/man1', ) executable( 'passimd', sources: [ 'passim-avahi.c', 'passim-avahi-service-browser.c', 'passim-avahi-service.c', 'passim-avahi-service-resolver.c', 'passim-common.c', 'passim-gnutls.c', 'passim-server.c', ], include_directories: [ root_incdir, passim_incdir, ], dependencies: [ libgio, libsoup, libgnutls, ], link_with: [ passim, ], install: true, install_dir: libexecdir, ) executable( 'passim', sources: [ 'passim-cli.c', 'passim-common.c', ], include_directories: [ root_incdir, passim_incdir, ], dependencies: [ libgio, libsoup, ], link_with: [ passim, ], install: true, install_dir: bindir, ) env = environment() env.set('G_TEST_SRCDIR', meson.current_source_dir()) env.set('G_TEST_BUILDDIR', meson.current_build_dir()) e = executable( 'passim-self-test', sources: [ 'passim-common.c', 'passim-self-test.c', ], include_directories: [ root_incdir, passim_incdir, ], dependencies: [ libgio ], link_with: [ passim ], c_args: [ '-DSRCDIR="' + meson.current_source_dir() + '"', '-DBUILDDIR="' + meson.current_build_dir() + '"', ], ) test('passim-self-test', e, is_parallel: false, timeout: 180, env: env) passim-0.1.10/src/org.freedesktop.Passim.xml000066400000000000000000000074021500514526500207400ustar00rootroot00000000000000 The interface used for interacting with Passim. The daemon version. The daemon web URI. The daemon auto-generated name, e.g. "Passim-0801". The daemon current status. The total number of bytes saved by using this project. The carbon saving in kg CO₂e by using this project. Gets the hashes of index. NOTE: This can only be called by any user. An array of vardict. Adds a file to the index. NOTE: This can only be called by the root user. The file descriptor to read. The attributes in a dictionary. Unpublish a file from the index. NOTE: This can only be called by the root user. The hash of the file to unpublish. Some value on the interface has changed. passim-0.1.10/src/passim-avahi-service-browser.c000066400000000000000000000155511500514526500215330ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-avahi-service-browser.h" #include "passim-avahi-service.h" #include "passim-avahi.h" typedef struct { GDBusProxy *proxy; GPtrArray *items; /* of PassimAvahiService */ gchar *hash; gchar *object_path; gulong signal_id; } PassimAvahiServiceBrowserHelper; static void passim_avahi_service_browser_helper_free(PassimAvahiServiceBrowserHelper *helper) { if (helper->signal_id > 0) g_signal_handler_disconnect(helper->proxy, helper->signal_id); if (helper->proxy != NULL) g_object_unref(helper->proxy); g_ptr_array_unref(helper->items); g_free(helper->hash); g_free(helper->object_path); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimAvahiServiceBrowserHelper, passim_avahi_service_browser_helper_free) static void passim_avahi_service_browser_free_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GVariant) val = NULL; PassimAvahiServiceBrowserHelper *helper = g_task_get_task_data(task); val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } if (helper->items->len == 0) { g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to find %s", helper->hash); return; } g_task_return_pointer(task, g_ptr_array_ref(helper->items), (GDestroyNotify)g_ptr_array_unref); } static void passim_avahi_service_browser_free(GTask *task) { PassimAvahiServiceBrowserHelper *helper = g_task_get_task_data(task); /* needed? */ if (helper->signal_id > 0) { g_signal_handler_disconnect(helper->proxy, helper->signal_id); helper->signal_id = 0; } g_dbus_proxy_call(helper->proxy, "Free", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, g_task_get_cancellable(task), passim_avahi_service_browser_free_cb, task); } static void passim_avahi_service_browser_signal_cb(GDBusProxy *proxy, const gchar *sender_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { GTask *task = G_TASK(user_data); PassimAvahiServiceBrowserHelper *helper = g_task_get_task_data(task); if (g_strcmp0(signal_name, "CacheExhausted") == 0) return; if (g_strcmp0(signal_name, "AllForNow") == 0) { passim_avahi_service_browser_free(task); return; } if (g_strcmp0(signal_name, "Failure") == 0) { const gchar *errmsg = NULL; g_variant_get(parameters, "(&s)", &errmsg); g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", errmsg); return; } if (g_strcmp0(signal_name, "ItemNew") == 0) { g_autoptr(PassimAvahiService) item = g_new0(PassimAvahiService, 1); g_variant_get(parameters, "(iisssu)", &item->interface, &item->protocol, &item->name, &item->type, &item->domain, &item->flags); if (item->flags & AVAHI_LOOKUP_RESULT_LOCAL) { g_debug("ignoring local result on interface %i", item->interface); return; } g_ptr_array_add(helper->items, g_steal_pointer(&item)); return; } g_warning("unhandled ServiceBrowser signal: %s %s", signal_name, g_variant_get_type_string(parameters)); } static void passim_avahi_service_browser_start_cb(GObject *source, GAsyncResult *res, gpointer user_data) { GTask *task = G_TASK(user_data); /* unref when we get the signal */ g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_task_return_error(task, g_steal_pointer(&error)); g_object_unref(task); return; } } static void passim_avahi_service_browser_new_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; PassimAvahiServiceBrowserHelper *helper = g_task_get_task_data(task); helper->proxy = g_dbus_proxy_new_for_bus_finish(res, &error); if (helper->proxy == NULL) { g_prefix_error(&error, "failed to use ServiceBrowser %s: ", helper->object_path); g_task_return_error(task, g_steal_pointer(&error)); return; } helper->signal_id = g_signal_connect(helper->proxy, "g-signal", G_CALLBACK(passim_avahi_service_browser_signal_cb), task); g_dbus_proxy_call(helper->proxy, "Start", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, g_task_get_cancellable(task), passim_avahi_service_browser_start_cb, g_object_ref(task)); } static void passim_avahi_service_browser_prepare_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; PassimAvahiServiceBrowserHelper *helper = g_task_get_task_data(task); val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_prefix_error(&error, "failed to create a new ServiceBrowser: "); g_task_return_error(task, g_steal_pointer(&error)); return; } g_variant_get(val, "(o)", &helper->object_path); g_debug("connecting to %s", helper->object_path); g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, NULL, "org.freedesktop.Avahi", helper->object_path, "org.freedesktop.Avahi.ServiceBrowser", g_task_get_cancellable(task), passim_avahi_service_browser_new_cb, g_object_ref(task)); } void passim_avahi_service_browser_async(GDBusProxy *proxy, const gchar *hash, AvahiProtocol protocol, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { g_autofree gchar *subtype = passim_avahi_build_subtype_for_hash(hash); g_autoptr(GTask) task = NULL; g_autoptr(PassimAvahiServiceBrowserHelper) helper = g_new0(PassimAvahiServiceBrowserHelper, 1); helper->hash = g_strdup(hash); helper->items = g_ptr_array_new_with_free_func((GDestroyNotify)passim_avahi_service_free); task = g_task_new(proxy, cancellable, callback, callback_data); g_task_set_task_data(task, g_steal_pointer(&helper), (GDestroyNotify)passim_avahi_service_browser_helper_free); g_dbus_proxy_call(proxy, "ServiceBrowserPrepare", g_variant_new("(iissu)", AVAHI_IF_UNSPEC, protocol, subtype, PASSIM_SERVER_DOMAIN, 0), /* flags */ G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, cancellable, passim_avahi_service_browser_prepare_cb, g_steal_pointer(&task)); } /* element-type: PassimAvahiService */ GPtrArray * passim_avahi_service_browser_finish(GAsyncResult *res, GError **error) { g_return_val_if_fail(res != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } passim-0.1.10/src/passim-avahi-service-browser.h000066400000000000000000000007361500514526500215370ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include "passim-avahi.h" void passim_avahi_service_browser_async(GDBusProxy *proxy, const gchar *hash, AvahiProtocol protocol, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data); GPtrArray * passim_avahi_service_browser_finish(GAsyncResult *res, GError **error); passim-0.1.10/src/passim-avahi-service-resolver.c000066400000000000000000000227171500514526500217130ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-avahi-service-resolver.h" #include "passim-avahi.h" typedef struct { gchar *object_path; gchar *signal_name; GVariant *parameters; } PassimAvahiSignal; static void passim_avahi_signal_free(PassimAvahiSignal *signal) { g_free(signal->object_path); g_free(signal->signal_name); g_variant_unref(signal->parameters); g_free(signal); } typedef struct { GDBusProxy *proxy; gchar *object_path; gchar *address; gulong signal_id; GDBusConnection *connection; /* no-ref -- not needed with new Avahi */ guint subscription_id; /* not needed with new Avahi */ GPtrArray *signals; /* element-type PassimAvahiSignal -- not needed with new Avahi */ } PassimAvahiServiceResolverHelper; static void passim_avahi_service_resolver_helper_free(PassimAvahiServiceResolverHelper *helper) { if (helper->signal_id > 0) g_signal_handler_disconnect(helper->proxy, helper->signal_id); if (helper->proxy != NULL) g_object_unref(helper->proxy); if (helper->subscription_id != 0) g_dbus_connection_signal_unsubscribe(helper->connection, helper->subscription_id); g_ptr_array_unref(helper->signals); g_free(helper->address); g_free(helper->object_path); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimAvahiServiceResolverHelper, passim_avahi_service_resolver_helper_free) static void passim_avahi_service_resolver_free_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GVariant) val = NULL; PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } g_task_return_pointer(task, g_steal_pointer(&helper->address), g_free); } static void passim_avahi_service_resolver_free(GTask *task) { PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); /* needed? */ if (helper->signal_id > 0) { g_signal_handler_disconnect(helper->proxy, helper->signal_id); helper->signal_id = 0; } g_dbus_proxy_call(helper->proxy, "Free", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, g_task_get_cancellable(task), passim_avahi_service_resolver_free_cb, task); } static void passim_avahi_service_resolver_signal(GTask *task, const gchar *signal_name, GVariant *parameters) { PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); if (g_strcmp0(signal_name, "Failure") == 0) { const gchar *errmsg = NULL; g_variant_get(parameters, "(&s)", &errmsg); g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", errmsg); return; } if (g_strcmp0(signal_name, "Found") == 0) { const gchar *host = NULL; guint16 port = 0; g_autoptr(GSocketAddress) socket_addr = NULL; g_variant_get(parameters, "(iissssisqaayu)", NULL, NULL, NULL, NULL, NULL, NULL, NULL, &host, &port, NULL, NULL); socket_addr = g_inet_socket_address_new_from_string(host, port); if (g_socket_address_get_family(socket_addr) == G_SOCKET_FAMILY_IPV6) { helper->address = g_strdup_printf("[%s]:%i", host, port); } else { helper->address = g_strdup_printf("%s:%i", host, port); } passim_avahi_service_resolver_free(task); return; } g_warning("unhandled ServiceResolver signal: %s %s", signal_name, g_variant_get_type_string(parameters)); } static void passim_avahi_service_resolver_signal_cb(GDBusProxy *proxy, const gchar *sender_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { GTask *task = G_TASK(user_data); PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); /* if we got here then we're either lucky or running with an Avahi that includes the * fix in https://github.com/lathiat/avahi/pull/468 */ if (helper->subscription_id != 0) { g_dbus_connection_signal_unsubscribe(helper->connection, helper->subscription_id); helper->subscription_id = 0; } passim_avahi_service_resolver_signal(task, signal_name, parameters); } static void passim_avahi_service_resolver_start_cb(GObject *source, GAsyncResult *res, gpointer user_data) { GTask *task = G_TASK(user_data); /* unref when we get the signal */ PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_task_return_error(task, g_steal_pointer(&error)); g_object_unref(task); return; } g_debug("started %s", helper->object_path); for (guint i = 0; i < helper->signals->len; i++) { PassimAvahiSignal *signal = g_ptr_array_index(helper->signals, i); if (g_strcmp0(signal->object_path, helper->object_path) != 0) { g_debug("ignoring %s from %s", signal->signal_name, helper->object_path); continue; } g_info("working around Ahavi bug: %s sent before Start(), see " "https://github.com/lathiat/avahi/pull/468", signal->signal_name); passim_avahi_service_resolver_signal(task, signal->signal_name, signal->parameters); } } static void passim_avahi_service_resolver_new_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); helper->proxy = g_dbus_proxy_new_for_bus_finish(res, &error); if (helper->proxy == NULL) { g_prefix_error(&error, "failed to use ServiceResolver %s: ", helper->object_path); g_task_return_error(task, g_steal_pointer(&error)); return; } helper->signal_id = g_signal_connect(helper->proxy, "g-signal", G_CALLBACK(passim_avahi_service_resolver_signal_cb), task); g_dbus_proxy_call(helper->proxy, "Start", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, g_task_get_cancellable(task), passim_avahi_service_resolver_start_cb, g_object_ref(task)); } static void passim_avahi_service_resolver_prepare_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; g_autoptr(GVariant) val = NULL; PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); if (val == NULL) { g_prefix_error(&error, "failed to create a new ServiceResolver: "); g_task_return_error(task, g_steal_pointer(&error)); return; } g_variant_get(val, "(o)", &helper->object_path); g_debug("connecting to %s", helper->object_path); g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, NULL, "org.freedesktop.Avahi", helper->object_path, "org.freedesktop.Avahi.ServiceResolver", g_task_get_cancellable(task), passim_avahi_service_resolver_new_cb, g_object_ref(task)); } static void passim_avahi_service_resolver_signal_fallback_cb(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { GTask *task = G_TASK(user_data); PassimAvahiServiceResolverHelper *helper = g_task_get_task_data(task); PassimAvahiSignal *signal = g_new0(PassimAvahiSignal, 1); signal->object_path = g_strdup(object_path); signal->signal_name = g_strdup(signal_name); signal->parameters = g_variant_ref(parameters); g_ptr_array_add(helper->signals, signal); } void passim_avahi_service_resolver_async(GDBusProxy *proxy, PassimAvahiService *service, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { g_autoptr(GTask) task = NULL; g_autoptr(PassimAvahiServiceResolverHelper) helper = g_new0(PassimAvahiServiceResolverHelper, 1); task = g_task_new(proxy, cancellable, callback, callback_data); /* work around a bug in Avahi, see https://github.com/lathiat/avahi/issues/446 */ helper->signals = g_ptr_array_new_with_free_func((GDestroyNotify)passim_avahi_signal_free); helper->connection = g_dbus_proxy_get_connection(proxy); helper->subscription_id = g_dbus_connection_signal_subscribe(helper->connection, g_dbus_proxy_get_name(proxy), "org.freedesktop.Avahi.ServiceResolver", NULL, NULL, NULL, /* argv */ G_DBUS_SIGNAL_FLAGS_NONE, passim_avahi_service_resolver_signal_fallback_cb, g_object_ref(task), (GDestroyNotify)g_object_unref); g_task_set_task_data(task, g_steal_pointer(&helper), (GDestroyNotify)passim_avahi_service_resolver_helper_free); g_dbus_proxy_call(proxy, "ServiceResolverPrepare", g_variant_new("(iisssiu)", service->interface, service->protocol, service->name, service->type, service->domain, service->protocol, 0), /* flags */ G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, cancellable, passim_avahi_service_resolver_prepare_cb, g_steal_pointer(&task)); } gchar * passim_avahi_service_resolver_finish(GAsyncResult *res, GError **error) { g_return_val_if_fail(res != NULL, NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } passim-0.1.10/src/passim-avahi-service-resolver.h000066400000000000000000000006751500514526500217170ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "passim-avahi-service.h" void passim_avahi_service_resolver_async(GDBusProxy *proxy, PassimAvahiService *service, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data); gchar * passim_avahi_service_resolver_finish(GAsyncResult *res, GError **error); passim-0.1.10/src/passim-avahi-service.c000066400000000000000000000011131500514526500200370ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-avahi-service.h" void passim_avahi_service_print(PassimAvahiService *service) { g_debug("Service { iface:%i, proto:%i, name:%s, type:%s, domain:%s, flags:%u }", service->interface, service->protocol, service->name, service->type, service->domain, service->flags); } void passim_avahi_service_free(PassimAvahiService *service) { g_free(service->name); g_free(service->type); g_free(service->domain); g_free(service); } passim-0.1.10/src/passim-avahi-service.h000066400000000000000000000007461500514526500200570ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include typedef struct { gint32 interface; gint32 protocol; gchar *name; gchar *type; gchar *domain; guint32 flags; } PassimAvahiService; void passim_avahi_service_free(PassimAvahiService *service); void passim_avahi_service_print(PassimAvahiService *service); G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimAvahiService, passim_avahi_service_free) passim-0.1.10/src/passim-avahi.c000066400000000000000000000256601500514526500164160ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-avahi-service-browser.h" #include "passim-avahi-service-resolver.h" #include "passim-avahi-service.h" #include "passim-avahi.h" struct _PassimAvahi { GObject parent_instance; gchar *name; GKeyFile *config; GDBusProxy *proxy; GDBusProxy *proxy_eg; }; G_DEFINE_TYPE(PassimAvahi, passim_avahi, G_TYPE_OBJECT) const gchar * passim_avahi_get_name(PassimAvahi *self) { return self->name; } static gchar * passim_avahi_truncate_hash(const gchar *hash) { return g_strndup(hash, 60); } gchar * passim_avahi_build_subtype_for_hash(const gchar *hash) { g_autofree gchar *truncated_hash = passim_avahi_truncate_hash(hash); return g_strdup_printf("_%s._sub.%s", truncated_hash, PASSIM_SERVER_TYPE); } static void passim_avahi_proxy_signal_cb(GDBusProxy *proxy, char *sender_name, char *signal_name, GVariant *parameters, gpointer user_data) { g_info("signal_name: %s %s", signal_name, g_variant_get_type_string(parameters)); } gboolean passim_avahi_connect(PassimAvahi *self, GError **error) { const gchar *object_path = NULL; g_autoptr(GVariant) object_pathv = NULL; g_return_val_if_fail(PASSIM_IS_AVAHI(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(self->proxy == NULL, FALSE); /* connect to daemon */ self->proxy = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.Avahi", "/", "org.freedesktop.Avahi.Server2", NULL, error); if (self->proxy == NULL) { g_prefix_error(error, "failed to contact Avahi: "); return FALSE; } g_signal_connect(self->proxy, "g-signal", G_CALLBACK(passim_avahi_proxy_signal_cb), self); /* create our entrygroup */ object_pathv = g_dbus_proxy_call_sync(self->proxy, "EntryGroupNew", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, NULL, error); if (object_pathv == NULL) { g_prefix_error(error, "failed to create a new entry group: "); return FALSE; } g_variant_get(object_pathv, "(&o)", &object_path); g_debug("connecting to %s", object_path); self->proxy_eg = g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.Avahi", object_path, "org.freedesktop.Avahi.EntryGroup", NULL, error); if (self->proxy_eg == NULL) { g_prefix_error(error, "failed to use EntryGroup %s: ", object_path); return FALSE; } g_signal_connect(self->proxy_eg, "g-signal", G_CALLBACK(passim_avahi_proxy_signal_cb), self); /* success */ return TRUE; } static gboolean passim_avahi_register_subtype(PassimAvahi *self, const gchar *hash, AvahiProtocol protocol, GError **error) { g_autofree gchar *subtype = passim_avahi_build_subtype_for_hash(hash); g_autoptr(GVariant) val = NULL; g_debug("adding subtype %s", subtype); val = g_dbus_proxy_call_sync(self->proxy_eg, "AddServiceSubtype", g_variant_new("(iiussss)", AVAHI_IF_UNSPEC, protocol, 0 /* flags */, self->name, PASSIM_SERVER_TYPE, PASSIM_SERVER_DOMAIN, subtype), G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, NULL, error); if (val == NULL) { g_prefix_error(error, "failed to add service subtype: "); return FALSE; } return TRUE; } gboolean passim_avahi_unregister(PassimAvahi *self, GError **error) { g_autoptr(GVariant) val1 = NULL; g_return_val_if_fail(PASSIM_IS_AVAHI(self), FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(self->proxy != NULL, FALSE); g_debug("resetting %s", self->name); val1 = g_dbus_proxy_call_sync(self->proxy_eg, "Reset", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, NULL, error); if (val1 == NULL) { g_prefix_error(error, "failed to reset entry group: "); return FALSE; } /* success */ return TRUE; } gboolean passim_avahi_register(PassimAvahi *self, gchar **keys, AvahiProtocol protocol, GError **error) { g_autoptr(GVariant) val2 = NULL; g_autoptr(GVariant) val4 = NULL; g_return_val_if_fail(PASSIM_IS_AVAHI(self), FALSE); g_return_val_if_fail(keys != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); g_return_val_if_fail(self->proxy != NULL, FALSE); if (!passim_avahi_unregister(self, error)) return FALSE; val2 = g_dbus_proxy_call_sync(self->proxy_eg, "AddService", g_variant_new("(iiussssqaay)", AVAHI_IF_UNSPEC, protocol, 0 /* flags */, self->name, PASSIM_SERVER_TYPE, PASSIM_SERVER_DOMAIN, PASSIM_SERVER_HOST, passim_config_get_port(self->config), NULL), G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, NULL, error); if (val2 == NULL) { g_prefix_error(error, "failed to add service: "); return FALSE; } for (guint i = 0; keys[i] != NULL; i++) { if (!passim_avahi_register_subtype(self, keys[i], protocol, error)) return FALSE; } val4 = g_dbus_proxy_call_sync(self->proxy_eg, "Commit", NULL, G_DBUS_CALL_FLAGS_NONE, PASSIM_SERVER_TIMEOUT, NULL, error); if (val4 == NULL) { g_prefix_error(error, "failed to commit entry group: "); return FALSE; } /* success */ return TRUE; } typedef struct { GDBusProxy *proxy; gchar *object_path; gchar *hash; gulong signal_id; GPtrArray *items; /* of PassimAvahiService */ GPtrArray *addresses; /* of utf-8 */ } PassimAvahiFindHelper; static void passim_avahi_service_resolve_next(GTask *task); static void passim_avahi_find_helper_free(PassimAvahiFindHelper *helper) { if (helper->proxy != NULL) g_object_unref(helper->proxy); if (helper->items != NULL) g_ptr_array_unref(helper->items); if (helper->addresses != NULL) g_ptr_array_unref(helper->addresses); g_free(helper->hash); g_free(helper->object_path); g_free(helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimAvahiFindHelper, passim_avahi_find_helper_free) static void passim_avahi_service_resolve_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autofree gchar *address = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = G_TASK(user_data); PassimAvahiFindHelper *helper = g_task_get_task_data(task); address = passim_avahi_service_resolver_finish(res, &error); if (address == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } if (g_ptr_array_find_with_equal_func(helper->addresses, address, g_str_equal, NULL)) { g_debug("already found %s, ignoring", address); } else { g_debug("new address %s, adding", address); g_ptr_array_add(helper->addresses, g_steal_pointer(&address)); } passim_avahi_service_resolve_next(g_steal_pointer(&task)); } static void passim_avahi_service_resolve_item(GTask *task, PassimAvahiService *item) { PassimAvahi *self = PASSIM_AVAHI(g_task_get_source_object(task)); g_debug( "ServiceResolverPrepare{ iface:%i, proto:%i, name:%s, type:%s, domain:%s, flags:%u }", item->interface, item->protocol, item->name, item->type, item->domain, item->flags); passim_avahi_service_resolver_async(self->proxy, item, g_task_get_cancellable(task), passim_avahi_service_resolve_cb, task); } static void passim_avahi_service_resolve_next(GTask *task) { PassimAvahiFindHelper *helper = g_task_get_task_data(task); PassimAvahiService *item; if (helper->items->len == 0) { if (helper->addresses->len > 0) { g_task_return_pointer(task, g_steal_pointer(&helper->addresses), (GDestroyNotify)g_ptr_array_unref); g_object_unref(task); return; } g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "cannot find hash"); g_object_unref(task); return; } /* resolve the next one */ item = g_ptr_array_steal_index(helper->items, 0); passim_avahi_service_resolve_item(task, item); } static void passim_avahi_service_browser_cb(GObject *source, GAsyncResult *res, gpointer user_data) { g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GError) error = NULL; PassimAvahiFindHelper *helper = g_task_get_task_data(task); helper->items = passim_avahi_service_browser_finish(res, &error); if (helper->items == NULL) { g_task_return_error(task, g_steal_pointer(&error)); return; } for (guint i = 0; i < helper->items->len; i++) { PassimAvahiService *item = g_ptr_array_index(helper->items, i); passim_avahi_service_print(item); } passim_avahi_service_resolve_next(g_steal_pointer(&task)); } void passim_avahi_find_async(PassimAvahi *self, const gchar *hash, AvahiProtocol protocol, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data) { g_autoptr(GTask) task = NULL; g_autofree gchar *truncated_hash = passim_avahi_truncate_hash(hash); g_autoptr(PassimAvahiFindHelper) helper = g_new0(PassimAvahiFindHelper, 1); g_return_if_fail(PASSIM_IS_AVAHI(self)); g_return_if_fail(hash != NULL); g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); g_return_if_fail(self->proxy != NULL); helper->hash = g_strdup(hash); helper->addresses = g_ptr_array_new_with_free_func(g_free); task = g_task_new(self, cancellable, callback, callback_data); g_task_set_task_data(task, g_steal_pointer(&helper), (GDestroyNotify)passim_avahi_find_helper_free); passim_avahi_service_browser_async(self->proxy, truncated_hash, protocol, cancellable, passim_avahi_service_browser_cb, g_steal_pointer(&task)); } /* element-type utf-8 */ GPtrArray * passim_avahi_find_finish(PassimAvahi *self, GAsyncResult *res, GError **error) { g_return_val_if_fail(PASSIM_IS_AVAHI(self), NULL); g_return_val_if_fail(g_task_is_valid(res, self), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); return g_task_propagate_pointer(G_TASK(res), error); } static void passim_avahi_init(PassimAvahi *self) { self->name = g_strdup_printf("%s-%04X", "Passim", (guint)g_random_int_range(0, G_MAXUINT16)); } static void passim_avahi_finalize(GObject *obj) { PassimAvahi *self = PASSIM_AVAHI(obj); g_free(self->name); g_key_file_unref(self->config); if (self->proxy != NULL) g_object_unref(self->proxy); if (self->proxy_eg != NULL) g_object_unref(self->proxy_eg); G_OBJECT_CLASS(passim_avahi_parent_class)->finalize(obj); } static void passim_avahi_class_init(PassimAvahiClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = passim_avahi_finalize; } PassimAvahi * passim_avahi_new(GKeyFile *config) { PassimAvahi *self; self = g_object_new(PASSIM_TYPE_AVAHI, NULL); self->config = g_key_file_ref(config); return PASSIM_AVAHI(self); } passim-0.1.10/src/passim-avahi.h000066400000000000000000000032271500514526500164160ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "passim-common.h" #define PASSIM_TYPE_AVAHI (passim_avahi_get_type()) G_DECLARE_FINAL_TYPE(PassimAvahi, passim_avahi, PASSIM, AVAHI, GObject) #define AVAHI_IF_UNSPEC -1 typedef enum { AVAHI_PROTO_INET = 0, /* IPv4 */ AVAHI_PROTO_INET6 = 1, /* IPv6 */ AVAHI_PROTO_UNSPEC = -1, } AvahiProtocol; typedef enum { AVAHI_LOOKUP_USE_WIDE_AREA = 1, AVAHI_LOOKUP_USE_MULTICAST = 2, AVAHI_LOOKUP_NO_TXT = 4, AVAHI_LOOKUP_NO_ADDRESS = 8, } AvahiLookupFlags; typedef enum { AVAHI_LOOKUP_RESULT_CACHED = 1, AVAHI_LOOKUP_RESULT_WIDE_AREA = 2, AVAHI_LOOKUP_RESULT_MULTICAST = 4, AVAHI_LOOKUP_RESULT_LOCAL = 8, AVAHI_LOOKUP_RESULT_OUR_OWN = 16, AVAHI_LOOKUP_RESULT_STATIC = 32, } AvahiLookupResultFlags; #define PASSIM_SERVER_DOMAIN "" #define PASSIM_SERVER_HOST "" #define PASSIM_SERVER_TYPE "_cache._tcp" #define PASSIM_SERVER_TIMEOUT 150 /* ms */ PassimAvahi * passim_avahi_new(GKeyFile *config); gboolean passim_avahi_connect(PassimAvahi *self, GError **error); gboolean passim_avahi_unregister(PassimAvahi *self, GError **error); gboolean passim_avahi_register(PassimAvahi *self, gchar **keys, AvahiProtocol protocol, GError **error); const gchar * passim_avahi_get_name(PassimAvahi *self); gchar * passim_avahi_build_subtype_for_hash(const gchar *hash); void passim_avahi_find_async(PassimAvahi *self, const gchar *hash, AvahiProtocol protocol, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer callback_data); GPtrArray * passim_avahi_find_finish(PassimAvahi *self, GAsyncResult *res, GError **error); passim-0.1.10/src/passim-cli.c000066400000000000000000000435001500514526500160660ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include "passim-common.h" typedef struct { PassimClient *client; gboolean next_reboot; } PassimCli; typedef gboolean (*PassimCliCmdFunc)(PassimCli *util, gchar **values, GError **error); typedef struct { gchar *name; gchar *arguments; gchar *description; PassimCliCmdFunc callback; } PassimCliCmd; static void passim_cli_cmd_free(PassimCliCmd *item) { g_free(item->name); g_free(item->arguments); g_free(item->description); g_free(item); } static void passim_cli_private_free(PassimCli *self) { if (self->client != NULL) g_object_unref(self->client); g_free(self); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimCli, passim_cli_private_free) #pragma clang diagnostic pop static GPtrArray * passim_cli_cmd_array_new(void) { return g_ptr_array_new_with_free_func((GDestroyNotify)passim_cli_cmd_free); } static gint passim_cli_cmd_sort_cb(PassimCliCmd **item1, PassimCliCmd **item2) { return g_strcmp0((*item1)->name, (*item2)->name); } static void passim_cli_cmd_array_sort(GPtrArray *array) { g_ptr_array_sort(array, (GCompareFunc)passim_cli_cmd_sort_cb); } static void passim_cli_cmd_array_add(GPtrArray *array, const gchar *name, const gchar *arguments, const gchar *description, PassimCliCmdFunc callback) { g_auto(GStrv) names = NULL; g_return_if_fail(name != NULL); g_return_if_fail(description != NULL); g_return_if_fail(callback != NULL); /* add each one */ names = g_strsplit(name, ",", -1); for (guint i = 0; names[i] != NULL; i++) { PassimCliCmd *item = g_new0(PassimCliCmd, 1); item->name = g_strdup(names[i]); if (i == 0) { item->description = g_strdup(description); } else { item->description = g_strdup_printf("Alias to %s", names[0]); } item->arguments = g_strdup(arguments); item->callback = callback; g_ptr_array_add(array, item); } } static gboolean passim_cli_cmd_array_run(GPtrArray *array, PassimCli *self, const gchar *command, gchar **values, GError **error) { g_auto(GStrv) values_copy = g_new0(gchar *, g_strv_length(values) + 1); /* clear out bash completion sentinel */ for (guint i = 0; values[i] != NULL; i++) { if (g_strcmp0(values[i], "{") == 0) break; values_copy[i] = g_strdup(values[i]); } /* find command */ for (guint i = 0; i < array->len; i++) { PassimCliCmd *item = g_ptr_array_index(array, i); if (g_strcmp0(item->name, command) == 0) return item->callback(self, values_copy, error); } /* not found */ g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "Command not found"); return FALSE; } static gchar * passim_cli_cmd_array_to_string(GPtrArray *array) { gsize len; const gsize max_len = 35; GString *string; /* print each command */ string = g_string_new(""); for (guint i = 0; i < array->len; i++) { PassimCliCmd *item = g_ptr_array_index(array, i); g_string_append(string, " "); g_string_append(string, item->name); len = strlen(item->name) + 2; if (item->arguments != NULL) { g_string_append(string, " "); g_string_append(string, item->arguments); len += strlen(item->arguments) + 1; } if (len < max_len) { for (gsize j = len; j < max_len + 1; j++) g_string_append_c(string, ' '); g_string_append(string, item->description); g_string_append_c(string, '\n'); } else { g_string_append_c(string, '\n'); for (gsize j = 0; j < max_len + 1; j++) g_string_append_c(string, ' '); g_string_append(string, item->description); g_string_append_c(string, '\n'); } } /* remove trailing newline */ if (string->len > 0) g_string_set_size(string, string->len - 1); return g_string_free(string, FALSE); } typedef struct { const gchar *key; gchar *value; } PassimItemAttr; static gchar * passim_cli_item_flag_to_string(PassimItemFlags flags) { const gchar *strv[4] = {NULL}; guint i = 0; if (flags & PASSIM_ITEM_FLAG_DISABLED) { /* TRANSLATORS: the item is not enabled */ strv[i++] = _("Disabled"); } if (flags & PASSIM_ITEM_FLAG_NEXT_REBOOT) { /* TRANSLATORS: only begin sharing the item after the next restart */ strv[i++] = _("Next Reboot"); } return g_strjoinv(", ", (gchar **)strv); } static GPtrArray * passim_cli_item_to_attrs(PassimItem *item) { GPtrArray *array = g_ptr_array_new_with_free_func(g_free); if (passim_item_get_basename(item) != NULL) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: item file basename */ attr->key = _("Filename"); attr->value = g_strdup(passim_item_get_basename(item)); g_ptr_array_add(array, attr); } if (passim_item_get_flags(item) != PASSIM_ITEM_FLAG_NONE) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: item flags */ attr->key = _("Flags"); attr->value = passim_cli_item_flag_to_string(passim_item_get_flags(item)); g_ptr_array_add(array, attr); } if (passim_item_get_cmdline(item) != NULL) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: basename of the thing that published the item */ attr->key = _("Command Line"); attr->value = g_strdup(passim_item_get_cmdline(item)); g_ptr_array_add(array, attr); } if (passim_item_get_max_age(item) != G_MAXUINT32) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: age of the published item */ attr->key = _("Age"); attr->value = g_strdup_printf("%u/%u", passim_item_get_age(item), passim_item_get_max_age(item)); g_ptr_array_add(array, attr); } if (passim_item_get_share_limit(item) != G_MAXUINT32) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: number of times we can share the item */ attr->key = _("Share Limit"); attr->value = g_strdup_printf("%u/%u", passim_item_get_share_count(item), passim_item_get_share_limit(item)); g_ptr_array_add(array, attr); } if (passim_item_get_size(item) != 0) { PassimItemAttr *attr = g_new0(PassimItemAttr, 1); /* TRANSLATORS: size of the published item */ attr->key = _("Size"); attr->value = g_format_size(passim_item_get_size(item)); g_ptr_array_add(array, attr); } return array; } static gchar * passim_cli_align_indent(const gchar *key, const gchar *value, guint indent) { GString *str = g_string_new(key); g_string_append_c(str, ':'); for (guint i = str->len; i < indent - 1; i++) g_string_append_c(str, ' '); g_string_append_c(str, ' '); g_string_append(str, value); return g_string_free(str, FALSE); } #define PASSIM_CLI_VALIGN 20 static gboolean passim_cli_status(PassimCli *self, gchar **values, GError **error) { PassimStatus status = passim_client_get_status(self->client); const gchar *status_value; gdouble download_saving = passim_client_get_download_saving(self->client); gdouble carbon_saving = passim_client_get_carbon_saving(self->client); g_autofree gchar *status_str = NULL; g_autoptr(GPtrArray) items = NULL; /* global status */ if (passim_client_get_name(self->client) != NULL) { g_autofree gchar *name_str = /* TRANSLATORS: the daemon autogenerated name, e.g. "Passim-ABCD" */ passim_cli_align_indent(_("Name"), passim_client_get_name(self->client), PASSIM_CLI_VALIGN); g_print("%s\n", name_str); } if (status == PASSIM_STATUS_STARTING || status == PASSIM_STATUS_LOADING) { /* TRANSLATORS: daemon is starting up */ status_value = _("Loading…"); } else if (status == PASSIM_STATUS_DISABLED_METERED) { /* TRANSLATORS: daemon is scared to publish files */ status_value = _("Disabled (metered network)"); } else if (status == PASSIM_STATUS_RUNNING) { /* TRANSLATORS: daemon is offering files like normal */ status_value = _("Running"); } else { status_value = passim_status_to_string(status); } /* TRANSLATORS: what the daemon is doing right now, e.g. 'Running' */ status_str = passim_cli_align_indent(_("Status"), status_value, PASSIM_CLI_VALIGN); g_print("%s\n", status_str); /* this is important enough to show */ if (download_saving > 0) { g_autofree gchar *download_value = g_format_size(download_saving); g_autofree gchar *download_str = /* TRANSLATORS: how many bytes we did not download from the internet */ passim_cli_align_indent(_("Network Saving"), download_value, PASSIM_CLI_VALIGN); g_print("%s\n", download_str); } if (carbon_saving > 0.001) { g_autofree gchar *carbon_value = g_strdup_printf("%.02lf kg CO₂e", carbon_saving); g_autofree gchar *carbon_str = /* TRANSLATORS: how much carbon we did not *burn* by using local data */ passim_cli_align_indent(_("Carbon Saving"), carbon_value, PASSIM_CLI_VALIGN); g_print("%s\n", carbon_str); } /* show location of the web console */ if (passim_client_get_uri(self->client) != NULL) { g_autofree gchar *uri_str = /* TRANSLATORS: full https://whatever of the daemon */ passim_cli_align_indent(_("URI"), passim_client_get_uri(self->client), PASSIM_CLI_VALIGN); g_print("%s\n", uri_str); } /* all items */ items = passim_client_get_items(self->client, error); if (items == NULL) return FALSE; for (guint i = 0; i < items->len; i++) { PassimItem *item = g_ptr_array_index(items, i); g_autoptr(GPtrArray) attrs = passim_cli_item_to_attrs(item); g_print("\n%s\n", passim_item_get_hash(item)); for (guint j = 0; j < attrs->len; j++) { PassimItemAttr *attr = g_ptr_array_index(attrs, j); g_autofree gchar *str = passim_cli_align_indent(attr->key, attr->value, PASSIM_CLI_VALIGN - 2); g_print("%s %s\n", j < attrs->len - 1 ? "├" : "└", str); } } /* success */ return TRUE; } static gboolean passim_cli_publish(PassimCli *self, gchar **values, GError **error) { g_autofree gchar *str = NULL; g_autoptr(PassimItem) item = passim_item_new(); /* parse args */ if (g_strv_length(values) < 1) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, /* TRANSLATORS: user mistyped the command */ _("Invalid arguments")); return FALSE; } if (!passim_item_load_filename(item, values[0], error)) return FALSE; if (g_strv_length(values) > 1) passim_item_set_max_age(item, g_ascii_strtoull(values[1], NULL, 10)); if (g_strv_length(values) > 2) passim_item_set_share_count(item, g_ascii_strtoull(values[2], NULL, 10)); if (self->next_reboot) passim_item_add_flag(item, PASSIM_ITEM_FLAG_NEXT_REBOOT); if (!passim_client_publish(self->client, item, error)) return FALSE; /* success */ str = passim_item_to_string(item); /* TRANSLATORS: now sharing to the world */ g_print("%s: %s\n", _("Published"), str); return TRUE; } static gboolean passim_cli_unpublish(PassimCli *self, gchar **values, GError **error) { /* parse args */ if (g_strv_length(values) != 1) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, /* TRANSLATORS: user mistyped the command */ _("Invalid arguments")); return FALSE; } if (!passim_client_unpublish(self->client, values[0], error)) return FALSE; /* TRANSLATORS: no longer sharing with the world */ g_print("%s: %s\n", _("Unpublished"), values[0]); return TRUE; } static gboolean passim_cli_accept_certificate_cb(SoupMessage *self, GTlsCertificate *tls_peer_certificate, GTlsCertificateFlags tls_peer_errors, gpointer user_data) { /* ignore self-signed certificate */ return TRUE; } static gboolean passim_cli_download(PassimCli *self, gchar **values, GError **error) { GInetAddress *inet_addr; GSocketAddress *socket_addr; SoupMessageHeaders *response_headers; SoupStatus status; const gchar *content_type; g_autofree gchar *checksum = NULL; g_autofree gchar *inet_addrstr = NULL; g_autofree gchar *remote_addr = NULL; g_autofree gchar *size = NULL; g_autofree gchar *uri = NULL; g_autofree gchar *user_agent = NULL; g_autoptr(GBytes) bytes = NULL; g_autoptr(SoupMessage) msg = NULL; g_autoptr(SoupSession) session = soup_session_new(); /* parse args */ if (g_strv_length(values) != 2) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, /* TRANSLATORS: user mistyped the command */ _("Invalid arguments, expected BASENAME HASH")); return FALSE; } /* download file */ user_agent = g_strdup_printf("%s/%s", PACKAGE_NAME, VERSION); soup_session_set_user_agent(session, user_agent); soup_session_set_timeout(session, 30); uri = g_strdup_printf("%s%s?sha256=%s&localhost=false", passim_client_get_uri(self->client), values[0], values[1]); msg = soup_message_new(SOUP_METHOD_GET, uri); g_signal_connect(msg, "accept-certificate", G_CALLBACK(passim_cli_accept_certificate_cb), self); bytes = soup_session_send_and_read(session, msg, NULL, error); if (bytes == NULL) return FALSE; /* get the host that provided the transfer */ socket_addr = soup_message_get_remote_address(msg); inet_addr = g_inet_socket_address_get_address(G_INET_SOCKET_ADDRESS(socket_addr)); inet_addrstr = g_inet_address_to_string(inet_addr); remote_addr = g_strdup_printf("%s:%u", inet_addrstr, g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(socket_addr))); /* verify error code */ status = soup_message_get_status(msg); if (!SOUP_STATUS_IS_SUCCESSFUL(status)) { if (g_utf8_validate((const gchar *)g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), NULL)) { g_debug("%s", (const gchar *)g_bytes_get_data(bytes, NULL)); } g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, /* TRANSLATORS: %1 is a filename, %2 is a internet address and %3 is the untranslated error phrase */ _("Failed to download %s from %s: %s"), values[0], remote_addr, soup_status_get_phrase(status)); return FALSE; } /* we HAVE to verify the checksum */ checksum = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, bytes); if (g_strcmp0(checksum, values[1]) != 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, /* TRANSLATORS: %1 is a filename, %2 is a internet address */ _("Failed to download %s from %s as file checksum does not match"), values[0], remote_addr); return FALSE; } /* save file */ if (!g_file_set_contents(values[0], (const gchar *)g_bytes_get_data(bytes, NULL), g_bytes_get_size(bytes), error)) return FALSE; /* success */ response_headers = soup_message_get_response_headers(msg); content_type = soup_message_headers_get_content_type(response_headers, NULL); size = g_format_size(g_bytes_get_size(bytes)); /* TRANSLATORS: %1 is a filename, %2 is size (e.g. '1.3 MB') and %3 is a type, (e.g. * 'application/zstd') */ g_print(_("Saved %s (%s of %s)"), values[0], size, content_type); g_print("\n"); return TRUE; } int main(int argc, char *argv[]) { gboolean version = FALSE; g_autofree gchar *cmd_descriptions = NULL; g_autoptr(PassimCli) self = g_new0(PassimCli, 1); g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = g_option_context_new(NULL); g_autoptr(GPtrArray) cmd_array = passim_cli_cmd_array_new(); const GOptionEntry options[] = { /* TRANSLATORS: --version */ {"version", '\0', 0, G_OPTION_ARG_NONE, &version, N_("Show project version"), NULL}, /* TRANSLATORS: only begin sharing the item after the next restart */ {"next-reboot", '\0', 0, G_OPTION_ARG_NONE, &self->next_reboot, N_("Next reboot"), NULL}, {NULL}, }; setlocale(LC_ALL, ""); bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); passim_cli_cmd_array_add(cmd_array, "status,dump", NULL, /* TRANSLATORS: CLI action description */ _("Show daemon status"), passim_cli_status); passim_cli_cmd_array_add(cmd_array, "publish", /* TRANSLATORS: CLI option example */ _("FILENAME [MAX-AGE] [MAX-SHARE]"), /* TRANSLATORS: CLI action description */ _("Publish an additional file"), passim_cli_publish); passim_cli_cmd_array_add(cmd_array, "unpublish", /* TRANSLATORS: CLI option example */ _("HASH"), /* TRANSLATORS: CLI action description */ _("Unpublish an existing file"), passim_cli_unpublish); passim_cli_cmd_array_add(cmd_array, "download", /* TRANSLATORS: CLI option example */ _("BASENAME HASH"), /* TRANSLATORS: CLI action description */ _("Download a file from a remote machine"), passim_cli_download); passim_cli_cmd_array_sort(cmd_array); cmd_descriptions = passim_cli_cmd_array_to_string(cmd_array); g_option_context_set_summary(context, cmd_descriptions); /* TRANSLATORS: CLI tool description */ g_option_context_set_description(context, _("Interact with the local passimd process.")); /* TRANSLATORS: CLI tool name */ g_set_application_name(_("Passim CLI")); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { /* TRANSLATORS: we don't know what to do */ g_printerr("%s: %s", _("Failed to parse arguments"), error->message); return EXIT_FAILURE; } /* connect to daemon */ self->client = passim_client_new(); if (!passim_client_load(self->client, &error)) { /* TRANSLATORS: daemon failed to start */ g_printerr("%s: %s", _("Failed to connect to daemon"), error->message); return EXIT_FAILURE; } /* just show versions and exit */ if (version) { /* TRANSLATORS: CLI tool */ g_print("%s: %s\n", _("client version"), VERSION); /* TRANSLATORS: server */ g_print("%s: %s\n", _("daemon version"), passim_client_get_version(self->client)); return EXIT_SUCCESS; } /* run command */ if (!passim_cli_cmd_array_run(cmd_array, self, argv[1], (gchar **)&argv[2], &error)) { g_dbus_error_strip_remote_error(error); g_printerr("%s\n", error->message); return EXIT_FAILURE; } return EXIT_SUCCESS; } passim-0.1.10/src/passim-common.c000066400000000000000000000177331500514526500166200ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include "passim-common.h" #define PASSIM_CONFIG_GROUP "daemon" #define PASSIM_CONFIG_PORT "Port" #define PASSIM_CONFIG_IPV6 "IPv6" #define PASSIM_CONFIG_PATH "Path" #define PASSIM_CONFIG_MAX_ITEM_SIZE "MaxItemSize" #define PASSIM_CONFIG_CARBON_COST "CarbonCost" const gchar * passim_status_to_string(PassimStatus status) { if (status == PASSIM_STATUS_STARTING) return "starting"; if (status == PASSIM_STATUS_LOADING) return "loading"; if (status == PASSIM_STATUS_DISABLED_METERED) return "disabled-metered"; if (status == PASSIM_STATUS_RUNNING) return "running"; return NULL; } GKeyFile * passim_config_load(GError **error) { g_autoptr(GKeyFile) kf = g_key_file_new(); g_autofree gchar *fn = g_build_filename(PACKAGE_SYSCONFDIR, "passim.conf", NULL); if (g_file_test(fn, G_FILE_TEST_EXISTS)) { if (!g_key_file_load_from_file(kf, fn, G_KEY_FILE_NONE, error)) return NULL; } else { g_debug("not loading %s as it does not exist", fn); } if (!g_key_file_has_key(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PORT, NULL)) g_key_file_set_integer(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PORT, 27500); if (!g_key_file_has_key(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_MAX_ITEM_SIZE, NULL)) { g_key_file_set_uint64(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_MAX_ITEM_SIZE, 100 * 1024 * 1024); } if (!g_key_file_has_key(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PATH, NULL)) { g_autofree gchar *path = g_build_filename(PACKAGE_LOCALSTATEDIR, "lib", PACKAGE_NAME, "data", NULL); g_key_file_set_string(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PATH, path); } return g_steal_pointer(&kf); } guint16 passim_config_get_port(GKeyFile *kf) { return g_key_file_get_integer(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PORT, NULL); } gboolean passim_config_get_ipv6(GKeyFile *kf) { return g_key_file_get_boolean(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_IPV6, NULL); } gsize passim_config_get_max_item_size(GKeyFile *kf) { return g_key_file_get_uint64(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_MAX_ITEM_SIZE, NULL); } gdouble passim_config_get_carbon_cost(GKeyFile *kf) { gdouble carbon_cost = g_key_file_get_double(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_CARBON_COST, NULL); if (carbon_cost < 0.00001) { /* using * https://www.carbonbrief.org/factcheck-what-is-the-carbon-footprint-of-streaming-video-on-netflix/ * we can see that 0.018 kg CO2e for 30 mins, where 3 GB/hr -- so this gives a * kg/GB of ~0.018 kg x (3h / 2) */ carbon_cost = 0.026367; } return carbon_cost; } gchar * passim_config_get_path(GKeyFile *kf) { return g_key_file_get_string(kf, PASSIM_CONFIG_GROUP, PASSIM_CONFIG_PATH, NULL); } gboolean passim_xattr_set_string(const gchar *filename, const gchar *name, const gchar *value, GError **error) { ssize_t rc = setxattr(filename, name, value, strlen(value), XATTR_CREATE); if (rc < 0) { g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to set %s: %s", name, strerror(errno)); return FALSE; } return TRUE; } gchar * passim_xattr_get_string(const gchar *filename, const gchar *name, GError **error) { ssize_t rc; g_autofree gchar *buf = NULL; rc = getxattr(filename, name, NULL, 0); if (rc < 0) { if (errno == ENODATA) return g_strdup(""); g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to get %s: %s", name, strerror(errno)); return NULL; } if (rc == 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid data for %s", name); return NULL; } /* copy out with appended NUL */ buf = g_new0(gchar, rc + 1); rc = getxattr(filename, name, buf, rc + 1); if (rc < 0) { g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to get %s: %s", name, strerror(errno)); return NULL; } return g_steal_pointer(&buf); } gboolean passim_xattr_set_uint32(const gchar *filename, const gchar *name, guint32 value, GError **error) { ssize_t rc = setxattr(filename, name, &value, sizeof(value), XATTR_CREATE); if (rc < 0) { g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to set %s: %s", name, strerror(errno)); return FALSE; } return TRUE; } guint32 passim_xattr_get_uint32(const gchar *filename, const gchar *name, guint32 value_fallback, GError **error) { guint32 value = 0; ssize_t rc = getxattr(filename, name, &value, sizeof(value)); if (rc < 0) { if (errno == ENODATA) { g_debug("using fallback %s=%u for %s", name, (guint)value_fallback, filename); return value_fallback; } g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to get %s: %s", name, strerror(errno)); return G_MAXUINT32; } if (value == G_MAXUINT32) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid data for %s", name); return G_MAXUINT32; } return value; } gboolean passim_mkdir(const gchar *dirname, GError **error) { g_return_val_if_fail(dirname != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); if (!g_file_test(dirname, G_FILE_TEST_IS_DIR)) g_debug("creating path %s", dirname); if (g_mkdir_with_parents(dirname, 0700) == -1) { g_set_error(error, G_IO_ERROR, g_io_error_from_errno(errno), "failed to create '%s': %s", dirname, g_strerror(errno)); return FALSE; } return TRUE; } gboolean passim_mkdir_parent(const gchar *filename, GError **error) { g_autofree gchar *parent = NULL; g_return_val_if_fail(filename != NULL, FALSE); g_return_val_if_fail(error == NULL || *error == NULL, FALSE); parent = g_path_get_dirname(filename); return passim_mkdir(parent, error); } GBytes * passim_load_input_stream(GInputStream *stream, gsize count, GError **error) { guint8 tmp[0x8000] = {0x0}; g_autoptr(GByteArray) buf = g_byte_array_new(); g_autoptr(GError) error_local = NULL; g_return_val_if_fail(G_IS_INPUT_STREAM(stream), NULL); g_return_val_if_fail(error == NULL || *error == NULL, NULL); /* this is invalid */ if (count == 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "A maximum read size must be specified"); return NULL; } /* read from stream in 32kB chunks */ while (TRUE) { gssize sz; sz = g_input_stream_read(stream, tmp, sizeof(tmp), NULL, &error_local); if (sz == 0) break; if (sz < 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, error_local->message); return NULL; } g_byte_array_append(buf, tmp, sz); if (buf->len > count) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE, "cannot read from fd: 0x%x > 0x%x", buf->len, (guint)count); return NULL; } } return g_bytes_new(buf->data, buf->len); } gchar * passim_get_boot_time(void) { g_autofree gchar *buf = NULL; g_auto(GStrv) lines = NULL; if (!g_file_get_contents("/proc/stat", &buf, NULL, NULL)) return NULL; lines = g_strsplit(buf, "\n", -1); for (guint i = 0; lines[i] != NULL; i++) { if (g_str_has_prefix(lines[i], "btime ")) return g_strdup(lines[i] + 6); } return NULL; } gboolean passim_file_set_contents(const gchar *filename, GBytes *bytes, GError **error) { gsize size = 0; const gchar *data = g_bytes_get_data(bytes, &size); g_debug("writing %s with %" G_GSIZE_FORMAT " bytes", filename, size); return g_file_set_contents_full(filename, data, size, G_FILE_SET_CONTENTS_CONSISTENT, 0600, error); } GBytes * passim_file_get_contents(const gchar *filename, GError **error) { gchar *data = NULL; gsize len = 0; if (!g_file_get_contents(filename, &data, &len, error)) return NULL; g_debug("reading %s with %" G_GSIZE_FORMAT " bytes", filename, len); return g_bytes_new_take(data, len); } passim-0.1.10/src/passim-common.h000066400000000000000000000025151500514526500166150ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include const gchar * passim_status_to_string(PassimStatus status); GKeyFile * passim_config_load(GError **error); guint16 passim_config_get_port(GKeyFile *kf); gboolean passim_config_get_ipv6(GKeyFile *kf); gsize passim_config_get_max_item_size(GKeyFile *kf); gdouble passim_config_get_carbon_cost(GKeyFile *kf); gchar * passim_config_get_path(GKeyFile *kf); gboolean passim_xattr_set_uint32(const gchar *filename, const gchar *name, guint32 value, GError **error); guint32 passim_xattr_get_uint32(const gchar *filename, const gchar *name, guint32 value_fallback, GError **error); gboolean passim_xattr_set_string(const gchar *filename, const gchar *name, const gchar *value, GError **error); gchar * passim_xattr_get_string(const gchar *filename, const gchar *name, GError **error); gboolean passim_mkdir(const gchar *dirname, GError **error); gboolean passim_mkdir_parent(const gchar *filename, GError **error); gchar * passim_get_boot_time(void); GBytes * passim_load_input_stream(GInputStream *stream, gsize count, GError **error); gboolean passim_file_set_contents(const gchar *filename, GBytes *bytes, GError **error); GBytes * passim_file_get_contents(const gchar *filename, GError **error); passim-0.1.10/src/passim-gnutls.c000066400000000000000000000216241500514526500166360ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "passim-gnutls.h" gnutls_x509_crt_t passim_gnutls_load_crt_from_blob(GBytes *blob, gnutls_x509_crt_fmt_t format, GError **error) { gnutls_datum_t d = {0}; int rc; g_auto(gnutls_x509_crt_t) crt = NULL; /* create certificate */ rc = gnutls_x509_crt_init(&crt); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* import the certificate */ d.size = g_bytes_get_size(blob); d.data = (unsigned char *)g_bytes_get_data(blob, NULL); rc = gnutls_x509_crt_import(crt, &d, format); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_import: %s [%i]", gnutls_strerror(rc), rc); return NULL; } return g_steal_pointer(&crt); } gnutls_privkey_t passim_gnutls_load_privkey_from_blob(GBytes *blob, GError **error) { int rc; gnutls_datum_t d = {0}; g_auto(gnutls_privkey_t) key = NULL; /* load the private key */ rc = gnutls_privkey_init(&key); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } d.size = g_bytes_get_size(blob); d.data = (unsigned char *)g_bytes_get_data(blob, NULL); rc = gnutls_privkey_import_x509_raw(key, &d, GNUTLS_X509_FMT_PEM, NULL, 0); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_import_x509_raw: %s [%i]", gnutls_strerror(rc), rc); return NULL; } return g_steal_pointer(&key); } gnutls_pubkey_t passim_gnutls_load_pubkey_from_privkey(gnutls_privkey_t privkey, GError **error) { g_auto(gnutls_pubkey_t) pubkey = NULL; int rc; /* get the public key part of the private key */ rc = gnutls_pubkey_init(&pubkey); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "pubkey_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } rc = gnutls_pubkey_import_privkey(pubkey, privkey, 0, 0); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "pubkey_import_privkey: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* success */ return g_steal_pointer(&pubkey); } gchar * passim_gnutls_datum_to_dn_str(const gnutls_datum_t *raw) { g_auto(gnutls_x509_dn_t) dn = NULL; g_autoptr(gnutls_datum_t) str = NULL; int rc; rc = gnutls_x509_dn_init(&dn); if (rc < 0) return NULL; rc = gnutls_x509_dn_import(dn, raw); if (rc < 0) return NULL; str = (gnutls_datum_t *)gnutls_malloc(sizeof(gnutls_datum_t)); str->data = NULL; rc = gnutls_x509_dn_get_str2(dn, str, 0); if (rc < 0) return NULL; return g_strndup((const gchar *)str->data, str->size); } /* generates a private key just like `certtool --generate-privkey` */ GBytes * passim_gnutls_create_private_key(GError **error) { gnutls_datum_t d = {0}; int bits; int key_type = GNUTLS_PK_RSA; int rc; g_auto(gnutls_x509_privkey_t) key = NULL; g_auto(gnutls_x509_spki_t) spki = NULL; g_autoptr(gnutls_data_t) d_payload = NULL; /* initialize key and SPKI */ rc = gnutls_x509_privkey_init(&key); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } rc = gnutls_x509_spki_init(&spki); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "spki_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* generate key */ bits = gnutls_sec_param_to_pk_bits(key_type, GNUTLS_SEC_PARAM_HIGH); g_debug("generating a %d bit %s private key...", bits, gnutls_pk_algorithm_get_name(key_type)); rc = gnutls_x509_privkey_generate2(key, key_type, bits, 0, NULL, 0); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_generate2: %s [%i]", gnutls_strerror(rc), rc); return NULL; } rc = gnutls_x509_privkey_verify_params(key); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_verify_params: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* save to file */ rc = gnutls_x509_privkey_export2(key, GNUTLS_X509_FMT_PEM, &d); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "privkey_export2: %s [%i]", gnutls_strerror(rc), rc); return NULL; } d_payload = d.data; return g_bytes_new(d_payload, d.size); } /* generates a self signed certificate just like: * `certtool --generate-self-signed --load-privkey priv.pem` */ GBytes * passim_gnutls_create_certificate(gnutls_privkey_t privkey, GError **error) { int rc; gnutls_datum_t d = {0}; guchar sha1buf[20]; gsize sha1bufsz = sizeof(sha1buf); g_auto(gnutls_pubkey_t) pubkey = NULL; g_auto(gnutls_x509_crt_t) crt = NULL; g_autoptr(gnutls_data_t) d_payload = NULL; /* load the public key from the private key */ pubkey = passim_gnutls_load_pubkey_from_privkey(privkey, error); if (pubkey == NULL) return NULL; /* create certificate */ rc = gnutls_x509_crt_init(&crt); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_init: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set public key */ rc = gnutls_x509_crt_set_pubkey(crt, pubkey); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_set_pubkey: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set positive random serial number */ rc = gnutls_rnd(GNUTLS_RND_NONCE, sha1buf, sizeof(sha1buf)); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "gnutls_rnd: %s [%i]", gnutls_strerror(rc), rc); return NULL; } sha1buf[0] &= 0x7f; rc = gnutls_x509_crt_set_serial(crt, sha1buf, sizeof(sha1buf)); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_set_serial: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set activation */ rc = gnutls_x509_crt_set_activation_time(crt, time(NULL)); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_activation_time: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set expiration */ rc = gnutls_x509_crt_set_expiration_time(crt, (time_t)-1); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_expiration_time: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set basic constraints */ rc = gnutls_x509_crt_set_basic_constraints(crt, 0, -1); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_basic_constraints: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set usage */ rc = gnutls_x509_crt_set_key_usage(crt, GNUTLS_KEY_DIGITAL_SIGNATURE); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_key_usage: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* make suitable for TLS */ rc = gnutls_x509_crt_set_key_purpose_oid(crt, GNUTLS_KP_TLS_WWW_SERVER, 0); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_key_purpose_oid: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set subject key ID */ rc = gnutls_x509_crt_get_key_id(crt, GNUTLS_KEYID_USE_SHA1, sha1buf, &sha1bufsz); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "get_key_id: %s [%i]", gnutls_strerror(rc), rc); return NULL; } rc = gnutls_x509_crt_set_subject_key_id(crt, sha1buf, sha1bufsz); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "set_subject_key_id: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* set version */ rc = gnutls_x509_crt_set_version(crt, 3); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "error setting certificate version: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* self-sign certificate */ rc = gnutls_x509_crt_privkey_sign(crt, crt, privkey, GNUTLS_DIG_SHA256, 0); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_privkey_sign: %s [%i]", gnutls_strerror(rc), rc); return NULL; } /* export to file */ rc = gnutls_x509_crt_export2(crt, GNUTLS_X509_FMT_PEM, &d); if (rc < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "crt_export2: %s [%i]", gnutls_strerror(rc), rc); return NULL; } d_payload = d.data; return g_bytes_new(d_payload, d.size); } passim-0.1.10/src/passim-gnutls.h000066400000000000000000000033441500514526500166420ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include #include #include #include typedef guchar gnutls_data_t; static void _gnutls_datum_deinit(gnutls_datum_t *d) { gnutls_free(d->data); gnutls_free(d); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_pkcs7_t, gnutls_pkcs7_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_privkey_t, gnutls_privkey_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_pubkey_t, gnutls_pubkey_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_crt_t, gnutls_x509_crt_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_dn_t, gnutls_x509_dn_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_privkey_t, gnutls_x509_privkey_deinit, NULL) G_DEFINE_AUTO_CLEANUP_FREE_FUNC(gnutls_x509_spki_t, gnutls_x509_spki_deinit, NULL) G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_data_t, gnutls_free) G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_pkcs7_signature_info_st, gnutls_pkcs7_signature_info_deinit) G_DEFINE_AUTOPTR_CLEANUP_FUNC(gnutls_datum_t, _gnutls_datum_deinit) #pragma clang diagnostic pop gchar * passim_gnutls_datum_to_dn_str(const gnutls_datum_t *raw); gnutls_x509_crt_t passim_gnutls_load_crt_from_blob(GBytes *blob, gnutls_x509_crt_fmt_t format, GError **error); gnutls_privkey_t passim_gnutls_load_privkey_from_blob(GBytes *blob, GError **error); gnutls_pubkey_t passim_gnutls_load_pubkey_from_privkey(gnutls_privkey_t privkey, GError **error); GBytes * passim_gnutls_create_private_key(GError **error); GBytes * passim_gnutls_create_certificate(gnutls_privkey_t privkey, GError **error); passim-0.1.10/src/passim-self-test.c000066400000000000000000000063021500514526500172240ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include "passim-common.h" #if 0 static GMainLoop *_test_loop = NULL; static guint _test_loop_timeout_id = 0; static gboolean passim_test_hang_check_cb(gpointer user_data) { g_main_loop_quit(_test_loop); _test_loop_timeout_id = 0; return G_SOURCE_REMOVE; } static void passim_test_loop_run_with_timeout(guint timeout_ms) { g_assert_cmpint(_test_loop_timeout_id, ==, 0); g_assert_null(_test_loop); _test_loop = g_main_loop_new(NULL, FALSE); _test_loop_timeout_id = g_timeout_add(timeout_ms, passim_test_hang_check_cb, NULL); g_main_loop_run(_test_loop); } static void passim_test_loop_quit(void) { if (_test_loop_timeout_id > 0) { g_source_remove(_test_loop_timeout_id); _test_loop_timeout_id = 0; } if (_test_loop != NULL) { g_main_loop_quit(_test_loop); g_main_loop_unref(_test_loop); _test_loop = NULL; } } #endif static void passim_common_func(void) { gboolean ret; guint32 value_u32; g_autofree gchar *boot_time = NULL; g_autofree gchar *value_str1 = NULL; g_autofree gchar *value_str2 = NULL; g_autofree gchar *xargs_fn = NULL; g_autofree gchar *xargs_path = NULL; g_autoptr(GError) error = NULL; /* ensure we got *something* */ boot_time = passim_get_boot_time(); g_assert_cmpstr(boot_time, !=, NULL); /* create dir for next step */ xargs_fn = g_test_build_filename(G_TEST_BUILT, "tests", "test.conf", NULL); xargs_path = g_path_get_dirname(xargs_fn); ret = passim_mkdir(xargs_path, &error); g_assert_no_error(error); g_assert_true(ret); ret = passim_mkdir(xargs_path, &error); g_assert_no_error(error); g_assert_true(ret); /* check xargs */ (void)g_unlink(xargs_fn); ret = g_file_set_contents(xargs_fn, "[daemon]", -1, &error); g_assert_no_error(error); g_assert_true(ret); ret = passim_xattr_set_uint32(xargs_fn, "user.test_u32", 123, &error); if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) { g_test_skip("no xattr support"); return; } g_assert_no_error(error); g_assert_true(ret); ret = passim_xattr_set_string(xargs_fn, "user.test_str", "hey", &error); g_assert_no_error(error); g_assert_true(ret); value_u32 = passim_xattr_get_uint32(xargs_fn, "user.test_u32", 456, &error); g_assert_no_error(error); g_assert_cmpint(value_u32, ==, 123); value_u32 = passim_xattr_get_uint32(xargs_fn, "user.test_MISSING", 456, &error); g_assert_no_error(error); g_assert_cmpint(value_u32, ==, 456); value_str1 = passim_xattr_get_string(xargs_fn, "user.test_str", &error); g_assert_no_error(error); g_assert_cmpstr(value_str1, ==, "hey"); value_str2 = passim_xattr_get_string(xargs_fn, "user.test_MISSING", &error); g_assert_no_error(error); g_assert_cmpstr(value_str2, ==, ""); } int main(int argc, char **argv) { (void)g_setenv("G_TEST_SRCDIR", SRCDIR, FALSE); (void)g_setenv("G_TEST_BUILDDIR", BUILDDIR, FALSE); g_test_init(&argc, &argv, NULL); /* only critical and error are fatal */ g_log_set_fatal_mask(NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); (void)g_setenv("G_MESSAGES_DEBUG", "all", TRUE); g_test_add_func("/passim/common", passim_common_func); return g_test_run(); } passim-0.1.10/src/passim-server.c000066400000000000000000001510061500514526500166260ustar00rootroot00000000000000/* * Copyright 2023 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include "passim-avahi.h" #include "passim-common.h" #include "passim-gnutls.h" typedef struct { GDBusConnection *connection; GDBusNodeInfo *introspection_daemon; GDBusProxy *proxy_uid; GHashTable *items; /* utf-8:PassimItem */ GFileMonitor *sysconfpkg_monitor; guint sysconfpkg_rescan_id; GKeyFile *kf; GMainLoop *loop; PassimAvahi *avahi; GNetworkMonitor *network_monitor; gchar *root; guint16 port; gboolean use_ipv6; guint owner_id; guint poll_item_age_id; guint timed_exit_id; guint64 download_saving; PassimStatus status; } PassimServer; static void passim_server_free(PassimServer *self) { if (self->sysconfpkg_rescan_id != 0) g_source_remove(self->sysconfpkg_rescan_id); if (self->poll_item_age_id != 0) g_source_remove(self->poll_item_age_id); if (self->timed_exit_id != 0) g_source_remove(self->timed_exit_id); if (self->loop != NULL) g_main_loop_unref(self->loop); if (self->avahi != NULL) g_object_unref(self->avahi); if (self->sysconfpkg_monitor != NULL) g_object_unref(self->sysconfpkg_monitor); if (self->items != NULL) g_hash_table_unref(self->items); if (self->kf != NULL) g_key_file_unref(self->kf); if (self->proxy_uid != NULL) g_object_unref(self->proxy_uid); if (self->connection != NULL) g_object_unref(self->connection); if (self->introspection_daemon != NULL) g_dbus_node_info_unref(self->introspection_daemon); g_free(self->root); g_free(self); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimServer, passim_server_free) static void passim_server_engine_changed(PassimServer *self) { /* not yet connected */ if (self->connection == NULL) return; g_dbus_connection_emit_signal(self->connection, NULL, PASSIM_DBUS_PATH, PASSIM_DBUS_INTERFACE, "Changed", NULL, NULL); } static void passim_server_emit_property_changed(PassimServer *self, const gchar *property_name, GVariant *property_value) { GVariantBuilder builder; GVariantBuilder invalidated_builder; /* not yet connected */ if (self->connection == NULL) { g_variant_unref(g_variant_ref_sink(property_value)); return; } /* build the dict */ g_variant_builder_init(&invalidated_builder, G_VARIANT_TYPE("as")); g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&builder, "{sv}", property_name, property_value); g_dbus_connection_emit_signal( self->connection, NULL, PASSIM_DBUS_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", g_variant_new("(sa{sv}as)", PASSIM_DBUS_INTERFACE, &builder, &invalidated_builder), NULL); g_variant_builder_clear(&builder); g_variant_builder_clear(&invalidated_builder); } static void passim_server_set_status(PassimServer *self, PassimStatus status) { /* sanity check */ if (self->status == status) return; self->status = status; g_debug("Emitting PropertyChanged('Status'='%s')", passim_status_to_string(status)); passim_server_emit_property_changed(self, "Status", g_variant_new_uint32(status)); passim_server_engine_changed(self); } static gboolean passim_server_avahi_register(PassimServer *self, GError **error) { guint keyidx = 0; g_autofree const gchar **keys = NULL; g_autoptr(GList) items = NULL; /* sanity check */ if (self->status == PASSIM_STATUS_STARTING) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED, "http server has not yet started"); return FALSE; } /* never publish when on a metered connection */ if (g_network_monitor_get_network_metered(self->network_monitor)) { g_info("on a metered connection, unregistering"); passim_server_set_status(self, PASSIM_STATUS_DISABLED_METERED); return passim_avahi_unregister(self->avahi, error); } /* build a GStrv of hashes */ items = g_hash_table_get_values(self->items); keys = g_new0(const gchar *, g_list_length(items) + 1); for (GList *l = items; l != NULL; l = l->next) { PassimItem *item = PASSIM_ITEM(l->data); if (passim_item_has_flag(item, PASSIM_ITEM_FLAG_DISABLED)) continue; keys[keyidx++] = passim_item_get_hash(item); } if (!passim_avahi_register(self->avahi, (gchar **)keys, self->use_ipv6 ? AVAHI_PROTO_UNSPEC : AVAHI_PROTO_INET, error)) return FALSE; /* success */ passim_server_set_status(self, PASSIM_STATUS_RUNNING); return TRUE; } static gchar * passim_server_get_logdir(void) { const gchar *logs_directory = g_getenv("LOGS_DIRECTORY"); if (logs_directory != NULL) return g_strdup(logs_directory); return g_build_filename(PACKAGE_LOCALSTATEDIR, "log", PACKAGE_NAME, NULL); } static gboolean passim_server_update_download_saving_arg(PassimServer *self, const gchar *arg, GError **error) { g_auto(GStrv) kvs = g_strsplit(arg, "=", -1); if (g_strcmp0(kvs[0], "size") == 0) { guint64 value = g_ascii_strtoull(kvs[1], NULL, 10); if (value != G_MAXUINT64 && value != 0) self->download_saving += value; } return TRUE; } static gboolean passim_server_update_download_saving_args(PassimServer *self, const gchar *args, GError **error) { g_auto(GStrv) sections = g_strsplit(args, ",", -1); for (guint i = 0; sections[i] != NULL; i++) { if (!passim_server_update_download_saving_arg(self, sections[i], error)) return FALSE; } return TRUE; } static gboolean passim_server_update_download_saving(PassimServer *self, GError **error) { g_autofree gchar *filename = NULL; g_autofree gchar *path = NULL; g_autoptr(GDataInputStream) dstream = NULL; g_autoptr(GError) error_local = NULL; g_autoptr(GFile) file = NULL; g_autoptr(GFileInputStream) istream = NULL; /* open file for reading */ path = passim_server_get_logdir(); filename = g_build_filename(path, "audit.log", NULL); file = g_file_new_for_path(filename); istream = g_file_read(file, NULL, &error_local); if (istream == NULL) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) return TRUE; g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } dstream = g_data_input_stream_new(G_INPUT_STREAM(istream)); /* read each line */ do { g_autofree gchar *str = NULL; g_autoptr(GError) error_line = NULL; g_auto(GStrv) sections = NULL; str = g_data_input_stream_read_line_utf8(dstream, NULL, NULL, &error_line); if (str == NULL && error_line != NULL) { g_propagate_error(error, g_steal_pointer(&error_line)); return FALSE; } if (str == NULL) break; sections = g_strsplit(str, " ", -1); if (g_strv_length(sections) >= 3 && g_strcmp0(sections[1], "SHARE") == 0) { if (!passim_server_update_download_saving_args(self, sections[2], error)) return FALSE; } } while (1); /* success */ return TRUE; } static gboolean passim_server_eventlog(PassimServer *self, const gchar *type, const gchar *value, GError **error) { g_autofree gchar *filename = NULL; g_autofree gchar *line = NULL; g_autofree gchar *path = NULL; g_autofree gchar *ts_iso8601 = NULL; g_autoptr(GDateTime) dt = g_date_time_new_now_utc(); g_autoptr(GFile) file = NULL; g_autoptr(GFileOutputStream) ostream = NULL; /* create logdir */ path = passim_server_get_logdir(); if (!passim_mkdir(path, error)) return FALSE; /* open file */ filename = g_build_filename(path, "audit.log", NULL); file = g_file_new_for_path(filename); ostream = g_file_append_to(file, G_FILE_CREATE_PRIVATE, NULL, error); if (ostream == NULL) return FALSE; /* create line */ ts_iso8601 = g_date_time_format_iso8601(dt); line = g_strdup_printf("%s %s %s\n", ts_iso8601, type, value); return g_output_stream_write_all(G_OUTPUT_STREAM(ostream), line, strlen(line), NULL, NULL, error); } static void passim_server_append_str(GString *str, const gchar *key, const gchar *value) { if (str->len > 0) g_string_append_c(str, ','); g_string_append_printf(str, "%s=%s", key, value); } static void passim_server_append_u64(GString *str, const gchar *key, guint64 value) { if (str->len > 0) g_string_append_c(str, ','); g_string_append_printf(str, "%s=%u", key, (guint)value); } static gboolean passim_server_add_item(PassimServer *self, PassimItem *item, GError **error) { g_debug("added https://localhost:%u/%s?sha256=%s", self->port, passim_item_get_basename(item), passim_item_get_hash(item)); g_hash_table_insert(self->items, g_strdup(passim_item_get_hash(item)), g_object_ref(item)); return TRUE; } static gboolean passim_item_load_bytes_nofollow(PassimItem *item, const gchar *filename, GError **error) { gint fd; g_autoptr(GBytes) bytes = NULL; g_autoptr(GInputStream) istream = NULL; g_autoptr(GMappedFile) mapped_file = NULL; /* load bytes from the fd to avoid TOCTOU */ fd = g_open(filename, O_NOFOLLOW, S_IRUSR); if (fd < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "skipping symlink %s", filename); return FALSE; } istream = g_unix_input_stream_new(fd, TRUE); if (istream == NULL) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "no stream to read from %s", filename); return FALSE; } mapped_file = g_mapped_file_new_from_fd(fd, FALSE, error); if (mapped_file == NULL) return FALSE; bytes = g_mapped_file_get_bytes(mapped_file); passim_item_set_bytes(item, bytes); return TRUE; } static gboolean passim_server_libdir_add(PassimServer *self, const gchar *filename, GError **error) { guint32 value; g_autofree gchar *basename = g_path_get_basename(filename); g_autofree gchar *boot_time = NULL; g_autofree gchar *cmdline = NULL; g_auto(GStrv) split = g_strsplit(basename, "-", 2); g_autoptr(PassimItem) item = passim_item_new(); /* this doesn't have to be a sha256 hash, but it has to be *something* */ if (g_strv_length(split) != 2) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, "expected {hash}-{filename} and got %s", basename); return FALSE; } /* create new item */ passim_item_set_basename(item, split[1]); if (!passim_item_load_bytes_nofollow(item, filename, error)) return FALSE; if (!passim_item_load_filename(item, filename, error)) return FALSE; /* not required now */ passim_item_set_bytes(item, NULL); /* get optional attributes */ value = passim_xattr_get_uint32(filename, "user.max_age", 24 * 60 * 60, error); if (value == G_MAXUINT32) return FALSE; passim_item_set_max_age(item, value); value = passim_xattr_get_uint32(filename, "user.share_limit", 5, error); if (value == G_MAXUINT32) return FALSE; passim_item_set_share_limit(item, value); cmdline = passim_xattr_get_string(filename, "user.cmdline", error); if (cmdline == NULL) return FALSE; passim_item_set_cmdline(item, cmdline); /* only allowed when rebooted */ boot_time = passim_xattr_get_string(filename, "user.boot_time", NULL); if (boot_time != NULL) { g_autofree gchar *boot_time_now = passim_get_boot_time(); if (g_strcmp0(boot_time_now, boot_time) == 0) { passim_item_add_flag(item, PASSIM_ITEM_FLAG_NEXT_REBOOT); passim_item_add_flag(item, PASSIM_ITEM_FLAG_DISABLED); } } return passim_server_add_item(self, item, error); } static gboolean passim_server_libdir_scan(PassimServer *self, GError **error) { g_autoptr(GDir) dir = NULL; const gchar *fn; /* sanity check */ if (!g_file_test(self->root, G_FILE_TEST_EXISTS)) { g_debug("not loading resources from %s as it does not exist", self->root); return TRUE; } g_debug("loading resources from %s", self->root); dir = g_dir_open(self->root, 0, error); if (dir == NULL) return FALSE; while ((fn = g_dir_read_name(dir)) != NULL) { g_autofree gchar *path = g_build_filename(self->root, fn, NULL); if (!passim_server_libdir_add(self, path, error)) return FALSE; } passim_server_engine_changed(self); return TRUE; } static gboolean passim_server_sysconfpkgdir_add(PassimServer *self, const gchar *filename, GError **error) { g_autofree gchar *hash = NULL; g_autoptr(PassimItem) item = passim_item_new(); /* get optional attributes */ hash = passim_xattr_get_string(filename, "user.checksum.sha256", NULL); if (hash != NULL && g_strcmp0(hash, "") != 0) passim_item_set_hash(item, hash); if (!passim_item_load_bytes_nofollow(item, filename, error)) return FALSE; if (!passim_item_load_filename(item, filename, error)) return FALSE; /* not required now */ passim_item_set_bytes(item, NULL); /* never delete these */ passim_item_set_max_age(item, G_MAXUINT32); passim_item_set_share_limit(item, G_MAXUINT32); /* save this for next time */ if (hash == NULL || g_strcmp0(hash, "") == 0) { passim_xattr_set_string(filename, "user.checksum.sha256", passim_item_get_hash(item), NULL); } return passim_server_add_item(self, item, error); } static gboolean passim_server_sysconfpkgdir_scan_path(PassimServer *self, const gchar *path, GError **error) { g_autoptr(GDir) dir = NULL; const gchar *fn; /* sanity check */ if (!g_file_test(path, G_FILE_TEST_EXISTS)) { g_debug("not loading resources from %s as it does not exist", path); return TRUE; } g_debug("scanning %s", path); dir = g_dir_open(path, 0, error); if (dir == NULL) return FALSE; while ((fn = g_dir_read_name(dir)) != NULL) { g_autofree gchar *fn_tmp = g_build_filename(path, fn, NULL); g_autoptr(GError) error_local = NULL; if (!passim_server_sysconfpkgdir_add(self, fn_tmp, &error_local)) { if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED)) { g_info("skipping %s as EPERM", fn_tmp); continue; } g_propagate_error(error, g_steal_pointer(&error_local)); return FALSE; } } return TRUE; } static gboolean passim_server_sysconfpkgdir_scan_keyfile(PassimServer *self, const gchar *filename, GError **error) { g_autofree gchar *path = NULL; g_autoptr(GKeyFile) kf = g_key_file_new(); if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_NONE, error)) return FALSE; path = g_key_file_get_string(kf, "passim", "Path", error); if (path == NULL) return FALSE; return passim_server_sysconfpkgdir_scan_path(self, path, error); } static gboolean passim_server_sysconfpkgdir_scan(PassimServer *self, GError **error); static gboolean passim_server_sysconfpkgdir_timeout_cb(gpointer user_data) { PassimServer *self = (PassimServer *)user_data; g_autoptr(GError) error_local1 = NULL; g_autoptr(GError) error_local2 = NULL; /* done */ self->sysconfpkg_rescan_id = 0; /* rescan and re-register */ if (!passim_server_sysconfpkgdir_scan(self, &error_local1)) g_printerr("failed to scan sysconfpkg directory: %s\n", error_local1->message); if (!passim_server_avahi_register(self, &error_local2)) g_warning("failed to register: %s", error_local2->message); return G_SOURCE_REMOVE; } static void passim_server_sysconfpkgdir_changed_cb(GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; /* rate limit */ if (self->sysconfpkg_rescan_id != 0) g_source_remove(self->sysconfpkg_rescan_id); self->sysconfpkg_rescan_id = g_timeout_add(500, passim_server_sysconfpkgdir_timeout_cb, self); } static gboolean passim_server_sysconfpkgdir_watch(PassimServer *self, GError **error) { g_autofree gchar *sysconfpkgdir = g_build_filename(PACKAGE_SYSCONFDIR, "passim.d", NULL); g_autoptr(GFile) file = g_file_new_for_path(sysconfpkgdir); self->sysconfpkg_monitor = g_file_monitor_directory(file, G_FILE_MONITOR_NONE, NULL, error); if (self->sysconfpkg_monitor == NULL) return FALSE; g_signal_connect(self->sysconfpkg_monitor, "changed", G_CALLBACK(passim_server_sysconfpkgdir_changed_cb), self); return TRUE; } static gboolean passim_server_sysconfpkgdir_scan(PassimServer *self, GError **error) { const gchar *fn; g_autofree gchar *sysconfpkgdir = g_build_filename(PACKAGE_SYSCONFDIR, "passim.d", NULL); g_autoptr(GDir) dir = NULL; g_autoptr(GList) items = g_hash_table_get_values(self->items); /* remove all existing sysconfpkgdir items */ for (GList *l = items; l != NULL; l = l->next) { PassimItem *item = PASSIM_ITEM(l->data); if (passim_item_get_cmdline(item) == NULL && passim_item_get_max_age(item) == G_MAXUINT32 && passim_item_get_share_limit(item) == G_MAXUINT32) { g_debug("removing %s due to rescan", passim_item_get_hash(item)); g_hash_table_remove(self->items, passim_item_get_hash(item)); } } /* sanity check */ if (!g_file_test(sysconfpkgdir, G_FILE_TEST_EXISTS)) { g_debug("not loading resources from %s as it does not exist", sysconfpkgdir); return TRUE; } g_debug("loading sysconfpkgdir config from %s", sysconfpkgdir); dir = g_dir_open(sysconfpkgdir, 0, error); if (dir == NULL) return FALSE; while ((fn = g_dir_read_name(dir)) != NULL) { g_autofree gchar *fn_tmp = g_build_filename(sysconfpkgdir, fn, NULL); if (!g_str_has_suffix(fn_tmp, ".conf")) continue; if (!passim_server_sysconfpkgdir_scan_keyfile(self, fn_tmp, error)) return FALSE; } passim_server_engine_changed(self); return TRUE; } typedef struct { PassimServer *self; SoupServerMessage *msg; gchar *hash; gchar *basename; gboolean allow_localhost; } PassimServerContext; static void passim_server_context_free(PassimServerContext *ctx) { if (ctx->msg != NULL) g_object_unref(ctx->msg); g_free(ctx->hash); g_free(ctx->basename); g_free(ctx); } G_DEFINE_AUTOPTR_CLEANUP_FUNC(PassimServerContext, passim_server_context_free) static void passim_server_msg_send_error(PassimServer *self, SoupServerMessage *msg, guint status_code, const gchar *reason) { g_autoptr(GString) html = g_string_new(NULL); const gchar *reason_fallback = reason ? reason : soup_status_get_phrase(status_code); g_string_append_printf(html, "%u %s" "%s", status_code, soup_status_get_phrase(status_code), reason_fallback); soup_server_message_set_status(msg, status_code, NULL); soup_server_message_set_response(msg, "text/html", SOUP_MEMORY_COPY, html->str, html->len); soup_server_message_unpause(msg); } static void passim_server_context_send_redirect(PassimServerContext *ctx, const gchar *location) { SoupMessageHeaders *hdrs = soup_server_message_get_response_headers(ctx->msg); g_autoptr(GString) html = g_string_new(NULL); g_autofree gchar *uri = g_strdup_printf("https://%s/%s?sha256=%s", location, ctx->basename, ctx->hash); g_string_append_printf(html, "Redirecting...", uri); soup_message_headers_append(hdrs, "Location", uri); soup_server_message_set_status(ctx->msg, SOUP_STATUS_MOVED_TEMPORARILY, NULL); soup_server_message_set_response(ctx->msg, "text/html", SOUP_MEMORY_COPY, html->str, html->len); soup_server_message_unpause(ctx->msg); } static void passim_server_send_index(PassimServer *self, SoupServerMessage *msg) { g_autoptr(GString) html = g_string_new(NULL); g_autoptr(GList) keys = g_hash_table_get_keys(self->items); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append( html, "\n"); g_string_append_printf(html, "%s\n", passim_avahi_get_name(self->avahi)); g_string_append(html, "\n"); g_string_append(html, ""); g_string_append(html, ""); g_string_append_printf(html, "

%s

\n", passim_avahi_get_name(self->avahi)); g_string_append_printf( html, "

A local caching server, " "version %s with status %s.

\n", PACKAGE_NAME, VERSION, passim_status_to_string(self->status)); if (keys == NULL) { g_string_append(html, "There are no shared files on this computer.\n"); } else { g_string_append(html, "

Shared Files:

\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); g_string_append(html, "\n"); for (GList *l = keys; l != NULL; l = l->next) { const gchar *hash = l->data; PassimItem *item = g_hash_table_lookup(self->items, hash); g_autofree gchar *flags = passim_item_get_flags_as_string(item); g_autofree gchar *url = g_strdup_printf("https://localhost:%u/%s?sha256=%s", self->port, passim_item_get_basename(item), hash); g_string_append(html, "\n"); g_string_append_printf(html, "\n", url, passim_item_get_basename(item)); g_string_append_printf(html, "\n", passim_item_get_hash(item)); if (passim_item_get_cmdline(item) == NULL) { g_string_append_printf(html, "\n"); } else { g_string_append_printf(html, "\n", passim_item_get_cmdline(item)); } if (passim_item_get_max_age(item) == G_MAXUINT32) { g_string_append_printf(html, "\n", passim_item_get_age(item) / 3600u); } else { g_string_append_printf(html, "\n", passim_item_get_age(item) / 3600u, passim_item_get_max_age(item) / 3600u); } if (passim_item_get_share_limit(item) == G_MAXUINT32) { g_string_append_printf(html, "\n", passim_item_get_share_count(item)); } else { g_string_append_printf(html, "\n", passim_item_get_share_count(item), passim_item_get_share_limit(item)); } if (passim_item_get_size(item) == 0) { g_string_append(html, "\n"); } else { g_autofree gchar *size = g_format_size(passim_item_get_size(item)); g_string_append_printf(html, "\n", size); } g_string_append_printf(html, "\n", flags); g_string_append(html, ""); } g_string_append(html, "
FilenameHashBinaryAgeSharedSizeFlags
%s%sn/a%s%u/∞h%u/%uh%u/∞%u/%u?%s%s
\n"); } g_string_append(html, "\n"); g_string_append(html, "\n"); soup_server_message_set_status(msg, SOUP_STATUS_OK, NULL); soup_server_message_set_response(msg, "text/html", SOUP_MEMORY_COPY, html->str, html->len); } static void passim_server_msg_send_file(PassimServer *self, SoupServerMessage *msg, const gchar *path) { SoupMessageHeaders *hdrs = soup_server_message_get_response_headers(msg); GMappedFile *mapping; g_autoptr(GBytes) bytes = NULL; g_autoptr(GError) error = NULL; g_autoptr(GFile) file = g_file_new_for_path(path); g_autoptr(GFileInfo) info = NULL; mapping = g_mapped_file_new(path, FALSE, &error); if (mapping == NULL) { soup_server_message_set_status(msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, error->message); return; } bytes = g_bytes_new_with_free_func(g_mapped_file_get_contents(mapping), g_mapped_file_get_length(mapping), (GDestroyNotify)g_mapped_file_unref, mapping); if (g_bytes_get_size(bytes) > 0) soup_message_body_append_bytes(soup_server_message_get_response_body(msg), bytes); soup_server_message_set_status(msg, SOUP_STATUS_OK, NULL); info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, NULL, &error); if (info == NULL) { soup_server_message_set_status(msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, error->message); return; } if (g_file_info_get_content_type(info) != NULL) { g_autofree gchar *mime_type = g_content_type_get_mime_type(g_file_info_get_content_type(info)); if (mime_type != NULL) soup_message_headers_append(hdrs, "Content-Type", mime_type); } } static gboolean passim_server_delete_item(PassimServer *self, PassimItem *item, GError **error) { g_autoptr(GString) event_msg = g_string_new(NULL); if (!g_file_delete(passim_item_get_file(item), NULL, error)) { g_prefix_error(error, "failed to delete %s: ", passim_item_get_hash(item)); return FALSE; } /* log */ passim_server_append_str(event_msg, "hash", passim_item_get_hash(item)); passim_server_append_str(event_msg, "basename", passim_item_get_basename(item)); if (!passim_server_eventlog(self, "DELTE", event_msg->str, error)) { g_prefix_error(error, "failed to log: "); return FALSE; } g_hash_table_remove(self->items, passim_item_get_hash(item)); if (!passim_server_avahi_register(self, error)) { g_prefix_error(error, "failed to register: "); return FALSE; } /* success */ return TRUE; } static void passim_server_msg_send_item(PassimServer *self, SoupServerMessage *msg, PassimItem *item) { SoupMessageHeaders *hdrs = soup_server_message_get_response_headers(msg); g_autofree gchar *content_disposition = NULL; g_autofree gchar *filename = NULL; g_autofree gchar *path = g_file_get_path(passim_item_get_file(item)); filename = g_uri_escape_string(passim_item_get_basename(item), NULL, TRUE); content_disposition = g_strdup_printf("attachment; filename=\"%s\"", filename); soup_message_headers_append(hdrs, "Content-Disposition", content_disposition); passim_server_msg_send_file(self, msg, path); passim_item_set_share_count(item, passim_item_get_share_count(item) + 1); } static void passim_server_check_share_limit(PassimServer *self, PassimItem *item) { /* we've shared this enough now */ if (passim_item_get_share_limit(item) > 0 && passim_item_get_share_count(item) >= passim_item_get_share_limit(item)) { g_autoptr(GError) error = NULL; g_debug("deleting %s as share limit reached", passim_item_get_hash(item)); if (!passim_server_delete_item(self, item, &error)) g_warning("failed: %s", error->message); } } static void passim_server_avahi_find_cb(GObject *source_object, GAsyncResult *res, gpointer data) { guint index_random; g_autoptr(GError) error = NULL; g_autoptr(GPtrArray) addresses = NULL; g_autoptr(PassimServerContext) ctx = (PassimServerContext *)data; addresses = passim_avahi_find_finish(PASSIM_AVAHI(source_object), res, &error); if (addresses == NULL) { passim_server_msg_send_error(ctx->self, ctx->msg, 404, error->message); return; } /* display all, and chose an option at random */ index_random = g_random_int_range(0, addresses->len); for (guint i = 0; i < addresses->len; i++) { const gchar *address = g_ptr_array_index(addresses, i); if (i == index_random) { g_info("chosen address: %s", address); passim_server_context_send_redirect(ctx, address); } else { g_info("ignore address: %s", address); } } } static gboolean passim_server_is_loopback(const gchar *inet_addr) { g_autoptr(GInetAddress) address = g_inet_address_new_from_string(inet_addr); return g_inet_address_get_is_loopback(address); } static void passim_server_handler_cb(SoupServer *server, SoupServerMessage *msg, const gchar *path, GHashTable *query, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; GInetAddress *inet_addr; GSocketAddress *socket_addr; PassimItem *item; GUri *uri = soup_server_message_get_uri(msg); gboolean allow_localhost = TRUE; gboolean is_loopback; g_autofree gchar *hash = NULL; g_autofree gchar *inet_addrstr = NULL; g_auto(GStrv) request = NULL; g_autoptr(PassimServerContext) ctx = g_new0(PassimServerContext, 1); /* only GET supported */ if (soup_server_message_get_method(msg) != SOUP_METHOD_GET) { passim_server_msg_send_error(self, msg, SOUP_STATUS_FORBIDDEN, NULL); return; } /* who is connecting */ socket_addr = soup_server_message_get_remote_address(msg); if (socket_addr == NULL) { passim_server_msg_send_error(self, msg, SOUP_STATUS_BAD_REQUEST, "failed to get client connection address"); return; } inet_addr = g_inet_socket_address_get_address(G_INET_SOCKET_ADDRESS(socket_addr)); inet_addrstr = g_inet_address_to_string(inet_addr); is_loopback = passim_server_is_loopback(inet_addrstr); g_info("accepting HTTP/1.%u %s %s %s from %s:%u (%s)", soup_server_message_get_http_version(msg), soup_server_message_get_method(msg), path, g_uri_get_query(uri) != NULL ? g_uri_get_query(uri) : "", inet_addrstr, g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(socket_addr)), is_loopback ? "loopback" : "remote"); /* just return the index */ if (g_strcmp0(path, "/") == 0) { if (!is_loopback) { passim_server_msg_send_error(self, msg, SOUP_STATUS_FORBIDDEN, NULL); return; } passim_server_send_index(self, msg); return; } if (g_strcmp0(path, "/favicon.ico") == 0 || g_strcmp0(path, "/style.css") == 0) { g_autofree gchar *fn = g_build_filename(PACKAGE_DATADIR, PACKAGE_NAME, path, NULL); if (!is_loopback) { passim_server_msg_send_error(self, msg, SOUP_STATUS_FORBIDDEN, NULL); return; } passim_server_msg_send_file(self, msg, fn); return; } /* find the request hash argument */ if (g_uri_get_query(uri) == NULL) { passim_server_msg_send_error(self, msg, SOUP_STATUS_BAD_REQUEST, NULL); return; } request = g_strsplit(g_uri_get_query(uri), "&", -1); for (guint i = 0; request[i] != NULL; i++) { g_auto(GStrv) kv = g_strsplit(request[i], "=", -1); if (g_strv_length(kv) != 2) continue; if (g_strcmp0(kv[0], "sha256") == 0) { if (hash != NULL) { passim_server_msg_send_error(self, msg, SOUP_STATUS_BAD_REQUEST, "duplicate sha256= argument"); return; } hash = g_strdup(kv[1]); continue; } if (g_strcmp0(kv[0], "localhost") == 0) { if (g_strcmp0(kv[1], "true") == 0) { allow_localhost = TRUE; continue; } if (g_strcmp0(kv[1], "false") == 0) { allow_localhost = FALSE; continue; } passim_server_msg_send_error( self, msg, SOUP_STATUS_BAD_REQUEST, "localhost option invalid, expected true|false"); return; } } if (hash == NULL) { passim_server_msg_send_error(self, msg, SOUP_STATUS_BAD_REQUEST, "sha256= argument required"); return; } if (!g_str_is_ascii(hash) || strlen(hash) != 64) { passim_server_msg_send_error(self, msg, SOUP_STATUS_NOT_ACCEPTABLE, "sha256 hash is malformed"); return; } /* already exists locally */ item = g_hash_table_lookup(self->items, hash); if (item != NULL && allow_localhost) { g_autoptr(GError) error = NULL; g_autoptr(GString) event_msg = g_string_new(NULL); if (passim_item_has_flag(item, PASSIM_ITEM_FLAG_DISABLED)) { passim_server_msg_send_error(self, msg, SOUP_STATUS_LOCKED, NULL); return; } passim_server_msg_send_item(self, msg, item); /* update counter */ self->download_saving += passim_item_get_size(item); /* log */ passim_server_append_str(event_msg, "hash", passim_item_get_hash(item)); passim_server_append_str(event_msg, "basename", passim_item_get_basename(item)); passim_server_append_u64(event_msg, "size", passim_item_get_size(item)); passim_server_append_str(event_msg, "ipaddr", inet_addrstr); if (!passim_server_eventlog(self, "SHARE", event_msg->str, &error)) g_warning("failed to log: %s", error->message); passim_server_check_share_limit(self, item); return; } /* only localhost is allowed to scan for hashes */ if (!is_loopback) { passim_server_msg_send_error(self, msg, SOUP_STATUS_FORBIDDEN, NULL); return; } /* create context */ ctx->self = self; ctx->allow_localhost = allow_localhost; ctx->msg = g_object_ref(msg); ctx->hash = g_strdup(hash); ctx->basename = g_path_get_basename(path); /* look for remote servers with this hash */ g_info("searching for %s", hash); soup_server_message_pause(msg); passim_avahi_find_async(self->avahi, hash, self->use_ipv6 ? AVAHI_PROTO_UNSPEC : AVAHI_PROTO_INET, NULL, passim_server_avahi_find_cb, g_steal_pointer(&ctx)); } static gboolean passim_server_publish_file(PassimServer *self, GBytes *blob, PassimItem *item, GError **error) { g_autofree gchar *hash = NULL; g_autofree gchar *localstate_dir = NULL; g_autofree gchar *localstate_filename = NULL; g_autofree gchar *hashed_filename = NULL; g_autoptr(GString) event_msg = g_string_new(NULL); g_autoptr(GFile) file = NULL; hash = g_compute_checksum_for_bytes(G_CHECKSUM_SHA256, blob); if (g_hash_table_contains(self->items, hash)) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, "%s already exists", hash); return FALSE; } hashed_filename = g_strdup_printf("%s-%s", hash, passim_item_get_basename(item)); localstate_dir = g_build_filename(PACKAGE_LOCALSTATEDIR, "lib", PACKAGE_NAME, "data", NULL); if (!passim_mkdir(localstate_dir, error)) return FALSE; localstate_filename = g_build_filename(localstate_dir, hashed_filename, NULL); if (g_file_test(localstate_filename, G_FILE_TEST_EXISTS)) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, "%s already exists", localstate_filename); return FALSE; } if (!g_file_set_contents(localstate_filename, g_bytes_get_data(blob, NULL), g_bytes_get_size(blob), error)) return FALSE; if (!passim_xattr_set_uint32(localstate_filename, "user.max_age", passim_item_get_max_age(item), error)) return FALSE; if (!passim_xattr_set_uint32(localstate_filename, "user.share_limit", passim_item_get_share_limit(item), error)) return FALSE; if (!passim_xattr_set_string(localstate_filename, "user.cmdline", passim_item_get_cmdline(item), error)) return FALSE; /* only allowed when rebooted */ if (passim_item_has_flag(item, PASSIM_ITEM_FLAG_NEXT_REBOOT)) { g_autofree gchar *boot_time = passim_get_boot_time(); if (!passim_xattr_set_string(localstate_filename, "user.boot_time", boot_time, error)) return FALSE; passim_item_add_flag(item, PASSIM_ITEM_FLAG_DISABLED); } /* add to interface */ file = g_file_new_for_path(localstate_filename); passim_item_set_hash(item, hash); passim_item_set_file(item, file); g_debug("added %s", localstate_filename); g_hash_table_insert(self->items, g_steal_pointer(&hash), g_object_ref(item)); if (!passim_server_avahi_register(self, error)) return FALSE; /* success */ passim_server_append_str(event_msg, "hash", passim_item_get_hash(item)); passim_server_append_str(event_msg, "basename", passim_item_get_basename(item)); passim_server_append_u64(event_msg, "size", passim_item_get_size(item)); passim_server_append_str(event_msg, "cmdline", passim_item_get_cmdline(item)); return passim_server_eventlog(self, "PBLSH", event_msg->str, error); } static gboolean passim_server_timed_exit_cb(gpointer user_data) { PassimServer *self = (PassimServer *)user_data; self->timed_exit_id = 0; g_main_loop_quit(self->loop); return G_SOURCE_REMOVE; } static void passim_server_check_item_age(PassimServer *self) { g_autoptr(GList) items = g_hash_table_get_values(self->items); g_debug("checking for max-age"); for (GList *l = items; l != NULL; l = l->next) { PassimItem *item = PASSIM_ITEM(l->data); guint32 age = passim_item_get_age(item); if (passim_item_get_max_age(item) == G_MAXUINT32) continue; if (age > passim_item_get_max_age(item)) { g_autoptr(GError) error = NULL; g_debug("deleting %s [%s] as max-age reached", passim_item_get_hash(item), passim_item_get_basename(item)); if (!passim_server_delete_item(self, item, &error)) g_warning("failed: %s", error->message); } else { g_debug("%s [%s] has age %uh, maximum is %uh", passim_item_get_hash(item), passim_item_get_basename(item), (guint)age / 3600u, passim_item_get_max_age(item) / 3600u); } } } static gboolean passim_server_check_item_age_cb(gpointer user_data) { PassimServer *self = (PassimServer *)user_data; passim_server_check_item_age(self); return G_SOURCE_CONTINUE; } static gchar * passim_server_sender_get_cmdline(PassimServer *self, const gchar *sender, GError **error) { guint value = G_MAXUINT; g_autofree gchar *cmdline_buf = NULL; g_autofree gchar *cmdline_fn = NULL; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_sync(self->proxy_uid, "GetConnectionUnixProcessID", g_variant_new("(s)", sender), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, error); if (val == NULL) { g_prefix_error(error, "failed to read user id of caller: "); return NULL; } g_variant_get(val, "(u)", &value); cmdline_fn = g_strdup_printf("/proc/%u/cmdline", value); if (!g_file_get_contents(cmdline_fn, &cmdline_buf, NULL, error)) { g_prefix_error(error, "failed to caller cmdline: "); return NULL; } return g_path_get_basename(cmdline_buf); } static gboolean passim_server_sender_check_uid(PassimServer *self, const gchar *sender, GError **error) { guint value = G_MAXUINT; g_autoptr(GVariant) val = NULL; val = g_dbus_proxy_call_sync(self->proxy_uid, "GetConnectionUnixUser", g_variant_new("(s)", sender), G_DBUS_CALL_FLAGS_NONE, 2000, NULL, error); if (val == NULL) { g_prefix_error(error, "failed to read user id of caller: "); return FALSE; } g_variant_get(val, "(u)", &value); if (value != 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "permission denied: UID %u != 0", value); return FALSE; } return TRUE; } static void passim_server_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; GVariant *val = NULL; if (g_strcmp0(method_name, "GetItems") == 0) { GVariantBuilder builder; g_autoptr(GList) items = g_hash_table_get_values(self->items); g_debug("Called %s()", method_name); g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}")); for (GList *l = items; l != NULL; l = l->next) { PassimItem *item = PASSIM_ITEM(l->data); g_variant_builder_add_value(&builder, passim_item_to_variant(item)); } val = g_variant_builder_end(&builder); g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); return; } if (g_strcmp0(method_name, "Publish") == 0) { GDBusMessage *message; GUnixFDList *fd_list; gint fd = 0; g_autofree gchar *cmdline = NULL; g_autoptr(GBytes) blob = NULL; g_autoptr(GDateTime) dt_now = g_date_time_new_now_utc(); g_autoptr(GError) error = NULL; g_autoptr(GInputStream) istream = NULL; g_autoptr(PassimItem) item = NULL; g_autoptr(GVariant) variant = NULL; g_variant_get(parameters, "(h@a{sv})", &fd, &variant); item = passim_item_from_variant(variant); g_debug("Called %s(%i, %s, 0x%x, %u, %u)", method_name, fd, passim_item_get_basename(item), (guint)passim_item_get_flags(item), passim_item_get_max_age(item), passim_item_get_share_limit(item)); /* only callable by root */ if (!passim_server_sender_check_uid(self, sender, &error)) { g_dbus_method_invocation_return_gerror(invocation, error); return; } /* record the binary that is publishing the file */ cmdline = passim_server_sender_get_cmdline(self, sender, &error); if (cmdline == NULL) { g_dbus_method_invocation_return_gerror(invocation, error); return; } passim_item_set_cmdline(item, cmdline); /* sanity check this does not contain a path */ if (g_strstr_len(passim_item_get_basename(item), -1, "/") != NULL) { g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "invalid basename"); return; } /* sanity check share values */ if (passim_item_get_share_count(item) >= passim_item_get_share_limit(item)) { g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "share count %u >= share-limit %u", passim_item_get_share_count(item), passim_item_get_share_limit(item)); return; } /* read from the file descriptor */ message = g_dbus_method_invocation_get_message(invocation); fd_list = g_dbus_message_get_unix_fd_list(message); if (fd_list == NULL || g_unix_fd_list_get_length(fd_list) != 1) { g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "invalid handle"); return; } fd = g_unix_fd_list_get(fd_list, 0, &error); if (fd < 0) { g_dbus_method_invocation_return_gerror(invocation, error); return; } /* read file */ istream = g_unix_input_stream_new(fd, TRUE); blob = passim_load_input_stream(istream, passim_config_get_max_item_size(self->kf), &error); if (blob == NULL) { if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE)) { g_autofree gchar *size = g_format_size(passim_config_get_max_item_size(self->kf)); g_dbus_method_invocation_return_error( invocation, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Failed to load file, size limit is %s", size); return; } g_dbus_method_invocation_return_gerror(invocation, error); return; } /* only set by daemon */ passim_item_set_ctime(item, dt_now); /* publish the new file */ if (!passim_server_publish_file(self, blob, item, &error)) { g_dbus_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); return; } if (g_strcmp0(method_name, "Unpublish") == 0) { const gchar *hash = NULL; PassimItem *item; g_autoptr(GError) error = NULL; /* only callable by root */ if (!passim_server_sender_check_uid(self, sender, &error)) { g_dbus_method_invocation_return_gerror(invocation, error); return; } g_variant_get(parameters, "(&s)", &hash); item = g_hash_table_lookup(self->items, hash); if (item == NULL) { g_dbus_method_invocation_return_error(invocation, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "%s not found", hash); return; } if (!passim_server_delete_item(self, item, &error)) { g_dbus_method_invocation_return_gerror(invocation, error); return; } g_dbus_method_invocation_return_value(invocation, NULL); return; } g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD, "no such method %s", method_name); } static gdouble passim_server_get_carbon_saving(PassimServer *self) { gdouble val = self->download_saving; /* convert both to MB */ val *= passim_config_get_carbon_cost(self->kf) / 1024; return val / 0x100000L; } static GVariant * passim_server_get_property(GDBusConnection *connection_, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; if (g_strcmp0(property_name, "DaemonVersion") == 0) return g_variant_new_string(SOURCE_VERSION); if (g_strcmp0(property_name, "Name") == 0) return g_variant_new_string(passim_avahi_get_name(self->avahi)); if (g_strcmp0(property_name, "Status") == 0) return g_variant_new_uint32(self->status); if (g_strcmp0(property_name, "DownloadSaving") == 0) return g_variant_new_uint64(self->download_saving); if (g_strcmp0(property_name, "CarbonSaving") == 0) return g_variant_new_double(passim_server_get_carbon_saving(self)); if (g_strcmp0(property_name, "Uri") == 0) { g_autofree gchar *uri = g_strdup_printf("https://localhost:%u/", self->port); return g_variant_new_string(uri); } /* return an error */ g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_PROPERTY, "failed to get daemon property %s", property_name); return NULL; } static void passim_server_register_object(PassimServer *self) { guint registration_id; static const GDBusInterfaceVTable interface_vtable = { .method_call = passim_server_method_call, .get_property = passim_server_get_property, NULL}; registration_id = g_dbus_connection_register_object(self->connection, PASSIM_DBUS_PATH, self->introspection_daemon->interfaces[0], &interface_vtable, self, /* user_data */ NULL, /* user_data_free_func */ NULL); /* GError** */ g_assert(registration_id > 0); } static void passim_server_dbus_bus_acquired_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; g_autoptr(GError) error = NULL; self->connection = g_object_ref(connection); passim_server_register_object(self); /* connect to D-Bus directly */ self->proxy_uid = g_dbus_proxy_new_sync(self->connection, G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, NULL, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", NULL, &error); if (self->proxy_uid == NULL) { g_warning("cannot connect to DBus: %s", error->message); return; } } static void passim_server_dbus_name_acquired_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { g_debug("acquired name: %s", name); } static void passim_server_dbus_name_lost_cb(GDBusConnection *connection, const gchar *name, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; g_warning("another service has claimed the dbus name %s", name); g_main_loop_quit(self->loop); } static gboolean passim_server_start_dbus(PassimServer *self, GError **error) { g_autofree gchar *introspection_fn = NULL; g_autofree gchar *introspection_xml = NULL; /* load introspection from file */ introspection_fn = g_build_filename(PACKAGE_DATADIR, "dbus-1", "interfaces", PASSIM_DBUS_INTERFACE ".xml", NULL); if (!g_file_get_contents(introspection_fn, &introspection_xml, NULL, error)) { g_prefix_error(error, "failed to read introspection: "); return FALSE; } self->introspection_daemon = g_dbus_node_info_new_for_xml(introspection_xml, error); if (self->introspection_daemon == NULL) { g_prefix_error(error, "failed to load introspection: "); return FALSE; } /* start D-Bus server */ self->owner_id = g_bus_own_name(G_BUS_TYPE_SYSTEM, PASSIM_DBUS_SERVICE, G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE, passim_server_dbus_bus_acquired_cb, passim_server_dbus_name_acquired_cb, passim_server_dbus_name_lost_cb, self, NULL); /* success */ return TRUE; } static GTlsCertificate * passim_server_load_tls_certificate(GError **error) { g_autofree gchar *cert_fn = NULL; g_autofree gchar *secret_fn = NULL; g_autoptr(GBytes) secret_blob = NULL; /* create secret key */ secret_fn = g_build_filename(PACKAGE_LOCALSTATEDIR, "lib", PACKAGE_NAME, "secret.key", NULL); if (!g_file_test(secret_fn, G_FILE_TEST_EXISTS)) { secret_blob = passim_gnutls_create_private_key(error); if (secret_blob == NULL) return NULL; if (!passim_mkdir_parent(secret_fn, error)) return NULL; if (!passim_file_set_contents(secret_fn, secret_blob, error)) return NULL; } /* create TLS cert */ cert_fn = g_build_filename(PACKAGE_LOCALSTATEDIR, "lib", PACKAGE_NAME, "cert.pem", NULL); if (!g_file_test(cert_fn, G_FILE_TEST_EXISTS)) { g_autoptr(GBytes) cert_blob = NULL; g_auto(gnutls_privkey_t) privkey = NULL; if (secret_blob == NULL) { secret_blob = passim_file_get_contents(secret_fn, error); if (secret_blob == NULL) return NULL; } privkey = passim_gnutls_load_privkey_from_blob(secret_blob, error); if (privkey == NULL) return NULL; cert_blob = passim_gnutls_create_certificate(privkey, error); if (cert_blob == NULL) return NULL; if (!passim_file_set_contents(cert_fn, cert_blob, error)) return NULL; } /* load cert */ g_info("using secret key %s and certificate %s", secret_fn, cert_fn); return g_tls_certificate_new_from_files(cert_fn, secret_fn, error); } static gboolean passim_server_sigint_cb(gpointer user_data) { PassimServer *self = (PassimServer *)user_data; g_debug("Handling SIGINT"); g_main_loop_quit(self->loop); return FALSE; } static void passim_server_network_monitor_metered_changed_cb(GNetworkMonitor *network_monitor, GParamSpec *pspec, gpointer user_data) { PassimServer *self = (PassimServer *)user_data; g_autoptr(GError) error_local = NULL; if (!passim_server_avahi_register(self, &error_local)) g_warning("failed to register: %s", error_local->message); } int main(int argc, char *argv[]) { gboolean version = FALSE; gboolean timed_exit = FALSE; g_autoptr(GError) error = NULL; g_autoptr(GOptionContext) context = g_option_context_new(NULL); g_autoptr(GSource) unix_signal_source = g_unix_signal_source_new(SIGINT); g_autoptr(GTlsCertificate) cert = NULL; g_autoptr(PassimServer) self = g_new0(PassimServer, 1); g_autoptr(SoupServer) soup_server = NULL; g_autoslist(GUri) uris = NULL; const GOptionEntry options[] = { {"version", '\0', 0, G_OPTION_ARG_NONE, &version, "Show project version", NULL}, {"timed-exit", '\0', 0, G_OPTION_ARG_NONE, &timed_exit, "Exit after a delay", NULL}, {NULL}}; (void)g_setenv("G_MESSAGES_DEBUG", "all", FALSE); (void)g_setenv("G_DEBUG", "fatal-criticals", FALSE); g_option_context_add_main_entries(context, options, NULL); if (!g_option_context_parse(context, &argc, &argv, &error)) { g_printerr("Failed to parse arguments: %s", error->message); return EXIT_FAILURE; } /* just show versions and exit */ if (version) { g_print("%s\n", VERSION); return EXIT_SUCCESS; } self->status = PASSIM_STATUS_STARTING; self->loop = g_main_loop_new(NULL, FALSE); self->kf = passim_config_load(&error); if (self->kf == NULL) { g_printerr("failed to load config: %s\n", error->message); return 1; } self->poll_item_age_id = g_timeout_add_seconds(60 * 60, passim_server_check_item_age_cb, self); if (timed_exit) self->timed_exit_id = g_timeout_add_seconds(10, passim_server_timed_exit_cb, self); self->avahi = passim_avahi_new(self->kf); self->port = passim_config_get_port(self->kf); self->use_ipv6 = passim_config_get_ipv6(self->kf); self->root = passim_config_get_path(self->kf); self->items = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_object_unref); self->network_monitor = g_network_monitor_get_default(); g_signal_connect(G_NETWORK_MONITOR(self->network_monitor), "notify::network-metered", G_CALLBACK(passim_server_network_monitor_metered_changed_cb), self); if (!passim_server_start_dbus(self, &error)) { g_warning("failed to register D-Bus: %s", error->message); return 1; } if (!passim_server_libdir_scan(self, &error)) { g_printerr("failed to scan directory: %s\n", error->message); return 1; } if (!passim_server_sysconfpkgdir_watch(self, &error)) { g_printerr("failed to watch sysconfpkg directory: %s\n", error->message); return 1; } if (!passim_server_sysconfpkgdir_scan(self, &error)) { g_printerr("failed to scan sysconfpkg directory: %s\n", error->message); return 1; } if (!passim_avahi_connect(self->avahi, &error)) { g_warning("failed to contact daemon: %s", error->message); return 1; } passim_server_check_item_age(self); /* set up the webserver */ cert = passim_server_load_tls_certificate(&error); if (cert == NULL) { g_warning("failed to load TLS cert: %s", error->message); return 1; } soup_server = soup_server_new("server-header", "passim ", "tls-certificate", cert, NULL); if (!soup_server_listen_all(soup_server, self->port, self->use_ipv6 ? SOUP_SERVER_LISTEN_HTTPS : SOUP_SERVER_LISTEN_HTTPS | SOUP_SERVER_LISTEN_IPV4_ONLY, &error)) { g_printerr("%s: %s\n", argv[0], error->message); return 1; } soup_server_add_handler(soup_server, NULL, passim_server_handler_cb, self, NULL); uris = soup_server_get_uris(soup_server); for (GSList *u = uris; u; u = u->next) { g_autofree gchar *str = g_uri_to_string(u->data); g_info("listening on %s", str); } self->status = PASSIM_STATUS_LOADING; /* update total shared so far */ if (!passim_server_update_download_saving(self, &error)) { g_warning("failed to read log: %s", error->message); return 1; } /* register objects with Avahi */ if (!passim_server_avahi_register(self, &error)) { g_warning("failed to register: %s", error->message); return 1; } /* do stuff on ctrl+c */ g_source_set_callback(unix_signal_source, passim_server_sigint_cb, self, NULL); g_source_attach(unix_signal_source, NULL); g_main_loop_run(self->loop); return 0; } passim-0.1.10/src/passim.1000066400000000000000000000010121500514526500152270ustar00rootroot00000000000000.TH passim 1 "0.1.0" "A local caching server" .SH NAME passim \- client control of the local caching server .SH SYNOPSIS .B passim [CMD] .SH DESCRIPTION This tool allows an administrator to display and share files using passim. .SH OPTIONS The passim command takes various options depending on the action. Run .B passim --help for the full list. .SH EXIT STATUS Commands that successfully execute will return "0", with generic failure as "1". .SH BUGS See GitHub Issues: Changes made: