mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-28 15:21:29 +05:30
Compare commits
268 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fd0550317 | |||
| 777084b6aa | |||
| ca5e83c83a | |||
| 7cd63786cf | |||
| 5fcff353e7 | |||
| 390d43e7e4 | |||
| ed71c9e95d | |||
| 8af787bb3e | |||
| 8b34acde14 | |||
| 815dddf301 | |||
| 1be1c1e190 | |||
| 73bc2ff95d | |||
| ae9f7a436a | |||
| 27baf766de | |||
| 9264cc84b4 | |||
| 28f86c4a07 | |||
| 6fe9815c68 | |||
| 16a8c2ce61 | |||
| 164ec8c494 | |||
| defdb5e2f5 | |||
| 28e11f3745 | |||
| e3d1da694f | |||
| 98a1b9ce53 | |||
| ca21698edc | |||
| 300c1b491f | |||
| 14b6d371c9 | |||
| bfd43cacbb | |||
| 7b43509f5b | |||
| 5ad51ee4a4 | |||
| 5b6a1880fa | |||
| 29038b4df4 | |||
| 955f6c518d | |||
| 0202e5a960 | |||
| 4e2b0ff14f | |||
| 8fe8d458f7 | |||
| af758c414c | |||
| dc6f4f5cb9 | |||
| 4e68cd824e | |||
| d520877610 | |||
| f0e4adbf36 | |||
| bf6137df6b | |||
| 2b4e08bca3 | |||
| 62deeb49c1 | |||
| 6ca9924445 | |||
| 806dc5f678 | |||
| a7b23cd2b5 | |||
| 178a3a4e57 | |||
| f5fd80af0f | |||
| ae163ade7b | |||
| b871f76540 | |||
| 50c1460bf5 | |||
| aecf6e5780 | |||
| ecf6c1ccc7 | |||
| b844409a12 | |||
| 9df2c7782a | |||
| 7a15b77bcb | |||
| 81f7ebebfd | |||
| 3f97cf3ce3 | |||
| 8b5e6dffc6 | |||
| 4741f33d28 | |||
| 78303955b8 | |||
| 9b0c242b5c | |||
| 057ed818a9 | |||
| b7a754b9ff | |||
| 62777f0887 | |||
| 9d04e49561 | |||
| bc131d729d | |||
| 84c3413e0e | |||
| fcb0b664b5 | |||
| cf4e41fbb0 | |||
| 53f7bdc3fa | |||
| d4602cd917 | |||
| ae84c25786 | |||
| 2adf3cf9aa | |||
| 1fdce4c733 | |||
| 962a779645 | |||
| ec5efac3ca | |||
| 8154b98959 | |||
| d075f3cecc | |||
| fdddb02817 | |||
| c07513a69d | |||
| 4142982fb8 | |||
| 860f1459e3 | |||
| d378d46316 | |||
| 4c97a761df | |||
| b61abf1cb8 | |||
| 85841ea0ce | |||
| c32e697324 | |||
| d770c60191 | |||
| 5095e43ecf | |||
| 00313838c6 | |||
| bcf656656f | |||
| fd27520bbd | |||
| 4849c2328b | |||
| 2e8005f3a7 | |||
| 5d4d5150ae | |||
| f9d3010165 | |||
| e7bd3957ae | |||
| cfd6d21216 | |||
| ab29a6c87b | |||
| 576efaa084 | |||
| 9d6cdddf25 | |||
| d8e34f8743 | |||
| b1d5bf0326 | |||
| 50b162c340 | |||
| 833d2c334f | |||
| 9651bf7802 | |||
| 26274da6cf | |||
| f4cdabf916 | |||
| c7514a0999 | |||
| 36a3c50452 | |||
| 4e957ecd1b | |||
| f7f8554b4c | |||
| 1a77fa667f | |||
| 8a779f2b11 | |||
| a767624e34 | |||
| a5569fbfff | |||
| 99bf48e5f8 | |||
| 1041211a6e | |||
| 8c804df56c | |||
| a7974e91c5 | |||
| ffe5a9835c | |||
| e79a8203ca | |||
| 4cdcdc29eb | |||
| 8438667191 | |||
| fc66c3f02c | |||
| 7810dc4db9 | |||
| 28e893e56d | |||
| 1161e9c157 | |||
| a17e9a013b | |||
| fe0e8e3a01 | |||
| c2ca986943 | |||
| 407e50d36e | |||
| 10845d8f94 | |||
| 43ae7f3412 | |||
| 1d9eaaa4ef | |||
| 7346714456 | |||
| ab3fc5283d | |||
| 713df50a2d | |||
| 7e7c5576ba | |||
| ad253f54a5 | |||
| fa991bcbeb | |||
| db1da9da5d | |||
| c23b2d6e4f | |||
| 13e62f3bbe | |||
| 2d241f3602 | |||
| 58c87f0952 | |||
| dbb72e35c3 | |||
| 6e5b45e59f | |||
| 2ed5a274b6 | |||
| 7dbcc1c472 | |||
| 6b9df3ebb5 | |||
| dc10c50685 | |||
| 896eb37d47 | |||
| a14669f4ff | |||
| 74840b3740 | |||
| d3ef442f59 | |||
| f64d6b131b | |||
| 1bb9aa3210 | |||
| 73b9e5d51f | |||
| 0c43fede6d | |||
| a941c5247f | |||
| 097f945536 | |||
| 9272dcb90a | |||
| ecacf8cdcf | |||
| 5be670a285 | |||
| 73aa8c0daa | |||
| c23afb6c37 | |||
| d622133060 | |||
| 12500fb753 | |||
| bf726a0656 | |||
| d9632f6a08 | |||
| 292722b503 | |||
| 979dcc4762 | |||
| f411a65c31 | |||
| 9629bd77ad | |||
| f642c26173 | |||
| 8fc330691f | |||
| 6a3e64c0b4 | |||
| 218f1787ac | |||
| 8396edb35f | |||
| a3d1ffcdb2 | |||
| c03d56cfdc | |||
| 7f484550bc | |||
| a90e3fa153 | |||
| ffb3232f6c | |||
| 1050848357 | |||
| 7d66ae02a6 | |||
| 13475b4387 | |||
| 91af1de9af | |||
| 4977273244 | |||
| a228f55121 | |||
| dbfb389f83 | |||
| bd83e02ad0 | |||
| a6e03c60ab | |||
| c842499594 | |||
| c1c7dd7011 | |||
| 83d38ea5e6 | |||
| 72391b37df | |||
| d0668939bb | |||
| a51d46e82c | |||
| ea29253d2a | |||
| 90bf5da1c9 | |||
| 6a2f53622b | |||
| 9f5511c8d5 | |||
| c740b990c2 | |||
| 372315b138 | |||
| c8c415f9f7 | |||
| a652f8e7e6 | |||
| 2860ca4b4d | |||
| 2103f1bcee | |||
| bdd92dd0d3 | |||
| 5944f8a485 | |||
| 0b9180511f | |||
| d440e5916e | |||
| 129017a3cd | |||
| 99079cafd2 | |||
| ae1d1ec013 | |||
| db0ac44c97 | |||
| 0dbcdb66a7 | |||
| c324fe0a6f | |||
| 19557f8fff | |||
| 94483d2d23 | |||
| 4c9fc240ed | |||
| f8e94d91c0 | |||
| 6f4e3abfc8 | |||
| ec695cee3c | |||
| fa89a70db3 | |||
| 14d46ffd7e | |||
| c20f883316 | |||
| 7a60d70114 | |||
| 814dbb3b5a | |||
| ed91395f20 | |||
| 5c5b09e7f8 | |||
| 8282d3336a | |||
| a7ff1c7d16 | |||
| 4a8c2d1d81 | |||
| 810dcd4ed5 | |||
| f09e79d741 | |||
| b21b69c8ee | |||
| 7f00c5c8bd | |||
| 1ed95cb020 | |||
| 88cb947f19 | |||
| 9ff5b6126b | |||
| 62d8c84526 | |||
| 2749e559d9 | |||
| 4ede49ca78 | |||
| a001d639ae | |||
| 9817a23b17 | |||
| 11e6d3772c | |||
| a649a36b27 | |||
| 9ddc7c7f86 | |||
| e6a96be547 | |||
| 069ceb8b56 | |||
| 7ce5629658 | |||
| 37c4a6b158 | |||
| da93839996 | |||
| da7f673a5e | |||
| d5c91a2f56 | |||
| e9656dd67f | |||
| 52f0cb6de6 | |||
| bde49b32dd | |||
| b3ec48855b | |||
| 0db06514b9 | |||
| d7835111fd | |||
| be409725f3 | |||
| 20454add32 | |||
| 5674caebaf |
@@ -0,0 +1,4 @@
|
||||
# Code of conduct
|
||||
|
||||
- [Moby community guidelines](https://github.com/moby/moby/blob/master/CONTRIBUTING.md#moby-community-guidelines)
|
||||
- [Docker Code of Conduct](https://github.com/docker/code-of-conduct)
|
||||
@@ -0,0 +1,289 @@
|
||||
# Contribute to this repository
|
||||
|
||||
This page contains information about reporting issues as well as some tips and
|
||||
guidelines useful to experienced open source contributors.
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
The project maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
**Please _DO NOT_ file a public issue**, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
We also like to send gifts—if you're into schwag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
|
||||
|
||||
## Reporting other issues
|
||||
|
||||
A great way to contribute to the project is to send a detailed report when you
|
||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
||||
and will thank you for it!
|
||||
|
||||
Check that [the issue database](https://github.com/docker/docker-credential-helpers/issues)
|
||||
doesn't already include that problem or suggestion before submitting an issue.
|
||||
If you find a match, you can use the "subscribe" button to get notified on
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help resolving it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolving the issue, please leave a comment.
|
||||
|
||||
Include the steps required to reproduce the problem if possible and applicable.
|
||||
This information will help us review and fix your issue faster. When sending
|
||||
lengthy log-files, consider posting them as an attachment, instead of posting
|
||||
inline.
|
||||
|
||||
**Do not forget to remove sensitive data from your logfiles before submitting**
|
||||
(you can replace those parts with "REDACTED").
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
||||
it? Do it! We will appreciate it.
|
||||
|
||||
If your pull request is not accepted on the first try, don't be discouraged! If
|
||||
there's a problem with the implementation, hopefully you received feedback on
|
||||
what to improve.
|
||||
|
||||
We're trying very hard to keep this project lean and focused. We don't want it to
|
||||
do everything for everybody. This means that we might decide against
|
||||
incorporating a new feature. However, there might be a way to implement that
|
||||
feature *on top of* code in this project.
|
||||
|
||||
### Design and cleanup proposals
|
||||
|
||||
You can propose new designs for existing features. You can also design
|
||||
entirely new features. We really appreciate contributors who want to refactor or
|
||||
otherwise cleanup our project.
|
||||
|
||||
### Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||
the below (from [developercertificate.org](https://developercertificate.org)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
**Use your real name** (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### Run the unit- and integration-tests
|
||||
|
||||
To validate PRs before submitting them you should run:
|
||||
|
||||
```bash
|
||||
$ make validate
|
||||
```
|
||||
|
||||
To run the tests:
|
||||
|
||||
```bash
|
||||
$ make test
|
||||
```
|
||||
|
||||
To generate new vendored files with go modules run:
|
||||
|
||||
```bash
|
||||
$ make vendor
|
||||
```
|
||||
|
||||
|
||||
### Conventions
|
||||
|
||||
- Fork the repository and make changes on your fork in a feature branch
|
||||
- Submit tests for your changes. See [run the unit- and integration-tests](#run-the-unit--and-integration-tests)
|
||||
for details.
|
||||
- [Sign your work](#sign-your-work)
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plug-ins that do this automatically.
|
||||
|
||||
Pull request descriptions should be as clear as possible and include a
|
||||
reference to all the issues that they address. Be sure that the [commit
|
||||
messages](#commit-messages) also contain the relevant information.
|
||||
|
||||
### Successful Changes
|
||||
|
||||
Before contributing large or high impact changes, make the effort to coordinate
|
||||
with the maintainers of the project before submitting a pull request. This
|
||||
prevents you from doing extra work that may or may not be merged.
|
||||
|
||||
Large PRs that are just submitted without any prior communication are unlikely
|
||||
to be successful.
|
||||
|
||||
While pull requests are the methodology for submitting changes to code, changes
|
||||
are much more likely to be accepted if they are accompanied by additional
|
||||
engineering work. While we don't define this explicitly, most of these goals
|
||||
are accomplished through communication of the design goals and subsequent
|
||||
solutions. Often times, it helps to first state the problem before presenting
|
||||
solutions.
|
||||
|
||||
Typically, the best methods of accomplishing this are to submit an issue,
|
||||
stating the problem. This issue can include a problem statement and a
|
||||
checklist with requirements. If solutions are proposed, alternatives should be
|
||||
listed and eliminated. Even if the criteria for elimination of a solution is
|
||||
frivolous, say so.
|
||||
|
||||
Larger changes typically work best with design documents. These are focused on
|
||||
providing context to the design at the time the feature was conceived and can
|
||||
inform future documentation contributions.
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Commit messages should follow best practices, including explaining the context
|
||||
of the problem and how it was solved, including in caveats or follow up changes
|
||||
required. They should tell the story of the change and provide readers
|
||||
understanding of what led to it.
|
||||
|
||||
If you're lost about what this even means, please see [How to Write a Git
|
||||
Commit Message](http://chris.beams.io/posts/git-commit/) for a start.
|
||||
|
||||
In practice, the best approach to maintaining a nice commit message is to
|
||||
leverage a `git add -p` and `git commit --amend` to formulate a solid
|
||||
changeset. This allows one to piece together a change, as information becomes
|
||||
available.
|
||||
|
||||
If you squash a series of commits, don't just submit that. Re-write the commit
|
||||
message, as if the series of commits was a single stroke of brilliance.
|
||||
|
||||
That said, there is no requirement to have a single commit for a PR, as long as
|
||||
each commit tells the story. For example, if there is a feature that requires a
|
||||
package, it might make sense to have the package in a separate commit then have
|
||||
a subsequent commit that uses it.
|
||||
|
||||
Remember, you're telling part of the story with the commit message. Don't make
|
||||
your chapter weird.
|
||||
|
||||
### Review
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
a comment after pushing. New commits show up in the pull request automatically,
|
||||
but the reviewers are notified only when you comment.
|
||||
|
||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
||||
mixed into the PR.
|
||||
|
||||
> **Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
||||
> feature branch to update your pull request rather than `merge master`.
|
||||
|
||||
Before you make a pull request, squash your commits into logical units of work
|
||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
||||
set of patches that should be reviewed together: for example, upgrading the
|
||||
version of a vendored dependency and taking advantage of its now available new
|
||||
feature constitute two separate units of work. Implementing a new function and
|
||||
calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
- After every commit, [make sure the test suite passes](#run-the-unit--and-integration-tests).
|
||||
Include documentation changes in the same pull request so that a revert would
|
||||
remove all traces of the feature or fix.
|
||||
- Include an issue reference like `closes #XXXX` or `fixes #XXXX` in the PR
|
||||
description that close an issue. Including references automatically closes
|
||||
the issue on a merge.
|
||||
- Do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
from the Git history.
|
||||
- See the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
|
||||
### Merge approval
|
||||
|
||||
Project maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
||||
indicate acceptance, or use the Github review approval feature.
|
||||
|
||||
|
||||
## Coding Style
|
||||
|
||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
||||
community. While some of these standards may seem arbitrary, they somehow seem
|
||||
to result in a solid, consistent codebase.
|
||||
|
||||
It is possible that the code base does not currently comply with these
|
||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
||||
goes against the spirit of the guidelines. All new contributions should make a
|
||||
best effort to clean up and make the code base better than they left it.
|
||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
||||
code base easier for humans to navigate and understand. Always keep that in
|
||||
mind when nudging others to comply.
|
||||
|
||||
The rules:
|
||||
|
||||
1. All code should be formatted with `gofmt -s`.
|
||||
2. All code should pass the default levels of
|
||||
[`golint`](https://github.com/golang/lint).
|
||||
3. All code should follow the guidelines covered in [Effective Go](http://golang.org/doc/effective_go.html)
|
||||
and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
4. Comment the code. Tell us the why, the history and the context.
|
||||
5. Document _all_ declarations and methods, even private ones. Declare
|
||||
expectations, caveats and anything else that may be important. If a type
|
||||
gets exported, having the comments already there will ensure it's ready.
|
||||
6. Variable name length should be proportional to its context and no longer.
|
||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
||||
In practice, short methods will have short variable names and globals will
|
||||
have longer names.
|
||||
7. No underscores in package names. If you need a compound name, step back,
|
||||
and re-examine why you need a compound name. If you still think you need a
|
||||
compound name, lose the underscore.
|
||||
8. No utils or helpers packages. If a function is not general enough to
|
||||
warrant its own package, it has not been written generally enough to be a
|
||||
part of a util package. Just leave it unexported and well-documented.
|
||||
9. All tests should run with `go test` and outside tooling should not be
|
||||
required. No, we don't need another unit testing framework. Assertion
|
||||
packages are acceptable if they provide _real_ incremental value.
|
||||
10. Even though we call these "rules" above, they are actually just
|
||||
guidelines. Since you've read all the rules, you now know that.
|
||||
|
||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||
[Go Blog](https://blog.golang.org) is also a great resource.
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
Please make sure you've read and understood our contributing guidelines;
|
||||
https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||
|
||||
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||
|
||||
For additional information on our contributing process, read our contributing
|
||||
guide https://docs.docker.com/opensource/code/
|
||||
|
||||
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||
"closes #xxxx"
|
||||
|
||||
Please provide the following information:
|
||||
-->
|
||||
|
||||
**- What I did**
|
||||
|
||||
**- How I did it**
|
||||
|
||||
**- How to verify it**
|
||||
|
||||
**- Description for the changelog**
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog:
|
||||
-->
|
||||
|
||||
|
||||
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Reporting security issues
|
||||
|
||||
The project maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
**Please _DO NOT_ file a public issue**, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated, and we will publicly thank you for it.
|
||||
We also like to send gifts—if you're into schwag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
open-pull-requests-limit: 10
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 2
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "bot"
|
||||
+59
-40
@@ -4,22 +4,26 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
DESTDIR: ./bin
|
||||
GO_VERSION: 1.18.5
|
||||
GO_VERSION: 1.25.9
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30 # guardrails timeout for the whole job
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -29,10 +33,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
-
|
||||
name: Run
|
||||
run: |
|
||||
@@ -40,33 +44,41 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30 # guardrails timeout for the whole job
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04
|
||||
- macOS-11
|
||||
- ubuntu-24.04
|
||||
- ubuntu-22.04
|
||||
- macOS-15-intel
|
||||
- macOS-15
|
||||
- macOS-14
|
||||
- windows-2022
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: true
|
||||
-
|
||||
name: Install deps
|
||||
if: ${{ matrix.os == 'ubuntu-20.04' }}
|
||||
name: Install deps (ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dbus-x11 gnome-keyring libsecret-1-dev pass
|
||||
-
|
||||
name: Install deps (macOS)
|
||||
if: startsWith(matrix.os, 'macOS-')
|
||||
run: |
|
||||
brew install pass
|
||||
-
|
||||
name: GPG conf
|
||||
if: ${{ matrix.os == 'ubuntu-20.04' }}
|
||||
uses: actions/github-script@v6
|
||||
if: ${{ !startsWith(matrix.os, 'windows-') }}
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
id: gpg
|
||||
with:
|
||||
script: |
|
||||
@@ -82,39 +94,40 @@ jobs:
|
||||
core.setOutput('passphrase', fs.readFileSync('.github/workflows/fixtures/7D851EB72D73BDA0.pass', {encoding: 'utf8'}));
|
||||
-
|
||||
name: Import GPG key
|
||||
if: ${{ matrix.os == 'ubuntu-20.04' }}
|
||||
uses: crazy-max/ghaction-import-gpg@v5
|
||||
if: ${{ !startsWith(matrix.os, 'windows-') }}
|
||||
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7.0.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.gpg.outputs.key }}
|
||||
passphrase: ${{ steps.gpg.outputs.passphrase }}
|
||||
trust_level: 5
|
||||
-
|
||||
name: Init pass
|
||||
if: ${{ !startsWith(matrix.os, 'windows-') }}
|
||||
run: |
|
||||
pass init 7D851EB72D73BDA0
|
||||
shell: bash
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
if [ "${{ matrix.os }}" = "ubuntu-20.04" ]; then
|
||||
echo -e "trust\n5\ny" | gpg --batch --no-tty --command-fd 0 --edit-key 7D851EB72D73BDA0
|
||||
pass init 7D851EB72D73BDA0
|
||||
fi
|
||||
go test -short -v -coverprofile=./coverage.txt -covermode=atomic ./...
|
||||
go tool cover -func=./coverage.txt
|
||||
make test COVERAGEDIR=${{ env.DESTDIR }}
|
||||
shell: bash
|
||||
-
|
||||
name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
file: ./coverage.txt
|
||||
files: ${{ env.DESTDIR }}/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
test-sandboxed:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30 # guardrails timeout for the whole job
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@v2
|
||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
||||
with:
|
||||
targets: test
|
||||
set: |
|
||||
@@ -122,24 +135,29 @@ jobs:
|
||||
*.cache-to=type=gha,scope=test,mode=max
|
||||
-
|
||||
name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
file: ${{ env.DESTDIR }}//coverage.txt
|
||||
files: ${{ env.DESTDIR }}//coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30 # guardrails timeout for the whole job
|
||||
permissions:
|
||||
# required to create GitHub release
|
||||
contents: write
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
-
|
||||
name: Build
|
||||
run: |
|
||||
@@ -157,7 +175,7 @@ jobs:
|
||||
find ${{ env.DESTDIR }} -type f -exec file -e ascii -e text -- {} +
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: docker-credential-helpers
|
||||
path: ${{ env.DESTDIR }}/*
|
||||
@@ -165,7 +183,7 @@ jobs:
|
||||
-
|
||||
name: GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -173,11 +191,12 @@ jobs:
|
||||
files: ${{ env.DESTDIR }}/*
|
||||
|
||||
build-deb:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30 # guardrails timeout for the whole job
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: zizmor
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: crazy-max/.github/.github/workflows/zizmor.yml@64a0bfaf6e6bb1c448d6e4c42b11034ee7094f16 # v1.7.1
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
min-severity: medium
|
||||
min-confidence: medium
|
||||
persona: pedantic
|
||||
+12
-24
@@ -1,35 +1,23 @@
|
||||
version: "2"
|
||||
run:
|
||||
timeout: 10m
|
||||
modules-download-mode: vendor
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- gofmt
|
||||
- govet
|
||||
- deadcode
|
||||
- depguard
|
||||
- goimports
|
||||
- ineffassign
|
||||
- misspell
|
||||
- unused
|
||||
- varcheck
|
||||
- revive
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- structcheck
|
||||
disable-all: true
|
||||
- unused
|
||||
settings:
|
||||
revive:
|
||||
rules:
|
||||
- name: package-comments # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments
|
||||
disabled: true
|
||||
|
||||
linters-settings:
|
||||
depguard:
|
||||
list-type: blacklist
|
||||
include-go-root: true
|
||||
packages:
|
||||
# The io/ioutil package has been deprecated.
|
||||
# https://go.dev/doc/go1.16#ioutil
|
||||
- io/ioutil
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- linters:
|
||||
- revive
|
||||
text: "stutters"
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
|
||||
+49
-50
@@ -1,11 +1,28 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.18.5
|
||||
ARG XX_VERSION=1.1.2
|
||||
ARG OSXCROSS_VERSION=11.3-r7-debian
|
||||
ARG GOLANGCI_LINT_VERSION=v1.47.3
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
# GO_VERSION sets the version of the golang base image to use.
|
||||
# It must be a valid tag in the docker.io/library/golang image repository.
|
||||
ARG GO_VERSION=1.25.9
|
||||
|
||||
# BASE_DEBIAN_DISTRO sets the golang base image debian variant to use.
|
||||
# It must be a valid variant in the docker.io/library/golang image repository.
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
# XX_VERSION sets the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.9.0
|
||||
|
||||
# OSXCROSS_VERSION sets the MacOSX cross toolchain to use.
|
||||
# It must be a valid tag in the docker.io/crazymax/osxcross image repository.
|
||||
ARG OSXCROSS_VERSION=11.3-r8-debian
|
||||
|
||||
# GOLANGCI_LINT_VERSION sets the version of the golangci-lint image to use.
|
||||
# It must be a valid tag in the docker.io/golangci/golangci-lint image repository.
|
||||
ARG GOLANGCI_LINT_VERSION=v2.11
|
||||
|
||||
# PACKAGE sets the package name to print in the "--version" output.
|
||||
# It sets the "github.com/docker/docker-credential-helpers/credentials.Package
|
||||
# variable at compile time.
|
||||
ARG PACKAGE=github.com/docker/docker-credential-helpers
|
||||
|
||||
# xx is a helper for cross-compilation
|
||||
@@ -14,9 +31,8 @@ FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
# osxcross contains the MacOSX cross toolchain for xx
|
||||
FROM crazymax/osxcross:${OSXCROSS_VERSION} AS osxcross
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bullseye AS gobase
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS gobase
|
||||
COPY --from=xx / /
|
||||
ARG DEBIAN_FRONTEND
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends clang dpkg-dev file git lld llvm make pkg-config rsync
|
||||
ENV GOFLAGS="-mod=vendor"
|
||||
ENV CGO_ENABLED="1"
|
||||
@@ -38,7 +54,8 @@ FROM scratch AS vendor-update
|
||||
COPY --from=vendored /out /
|
||||
|
||||
FROM vendored AS vendor-validate
|
||||
RUN --mount=type=bind,target=.,rw <<EOT
|
||||
RUN --mount=target=/context \
|
||||
--mount=target=.,type=tmpfs <<EOT
|
||||
set -e
|
||||
rsync -a /context/. .
|
||||
git add -A
|
||||
@@ -53,8 +70,7 @@ EOT
|
||||
|
||||
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
FROM gobase AS lint
|
||||
ARG DEBIAN_FRONTEND
|
||||
RUN apt-get install -y binutils gcc libc6-dev libgcc-10-dev libsecret-1-dev pkg-config
|
||||
RUN apt-get install -y binutils gcc libc6-dev libgcc-11-dev libsecret-1-dev pkg-config
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=from=golangci-lint,source=/usr/bin/golangci-lint,target=/usr/bin/golangci-lint \
|
||||
@@ -62,11 +78,9 @@ RUN --mount=type=bind,target=. \
|
||||
|
||||
FROM gobase AS base
|
||||
ARG TARGETPLATFORM
|
||||
ARG DEBIAN_FRONTEND
|
||||
RUN xx-apt-get install -y binutils gcc libc6-dev libgcc-10-dev libsecret-1-dev pkg-config
|
||||
RUN xx-apt-get install -y binutils gcc libc6-dev libgcc-11-dev libsecret-1-dev pkg-config
|
||||
|
||||
FROM base AS test
|
||||
ARG DEBIAN_FRONTEND
|
||||
RUN xx-apt-get install -y dbus-x11 gnome-keyring gpg-agent gpgconf libsecret-1-dev pass
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
@@ -85,8 +99,8 @@ RUN --mount=type=bind,target=. \
|
||||
gpg -k
|
||||
|
||||
mkdir /out
|
||||
xx-go test -short -v -coverprofile=/out/coverage.txt -covermode=atomic ./...
|
||||
xx-go tool cover -func=/out/coverage.txt
|
||||
xx-go --wrap
|
||||
make test COVERAGEDIR=/out
|
||||
EOT
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
@@ -96,21 +110,7 @@ FROM gobase AS version
|
||||
RUN --mount=target=. \
|
||||
echo -n "$(./hack/git-meta version)" | tee /tmp/.version ; echo -n "$(./hack/git-meta revision)" | tee /tmp/.revision
|
||||
|
||||
FROM base AS build-linux
|
||||
ARG PACKAGE
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=bind,source=/tmp/.version,target=/tmp/.version,from=version \
|
||||
--mount=type=bind,source=/tmp/.revision,target=/tmp/.revision,from=version <<EOT
|
||||
set -ex
|
||||
xx-go --wrap
|
||||
make build-pass build-secretservice PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
xx-verify /out/docker-credential-pass
|
||||
xx-verify /out/docker-credential-secretservice
|
||||
EOT
|
||||
|
||||
FROM base AS build-darwin
|
||||
FROM base AS build
|
||||
ARG PACKAGE
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
@@ -119,29 +119,28 @@ RUN --mount=type=bind,target=. \
|
||||
--mount=type=bind,source=/tmp/.version,target=/tmp/.version,from=version \
|
||||
--mount=type=bind,source=/tmp/.revision,target=/tmp/.revision,from=version <<EOT
|
||||
set -ex
|
||||
export MACOSX_VERSION_MIN=$(make print-MACOSX_DEPLOYMENT_TARGET)
|
||||
xx-go --wrap
|
||||
go install std
|
||||
make build-osxkeychain build-pass PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
xx-verify /out/docker-credential-osxkeychain
|
||||
xx-verify /out/docker-credential-pass
|
||||
case "$(xx-info os)" in
|
||||
linux)
|
||||
make build-pass build-secretservice PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
xx-verify /out/docker-credential-pass
|
||||
xx-verify /out/docker-credential-secretservice
|
||||
;;
|
||||
darwin)
|
||||
go install std
|
||||
make build-osxkeychain build-pass PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
xx-verify /out/docker-credential-osxkeychain
|
||||
xx-verify /out/docker-credential-pass
|
||||
;;
|
||||
windows)
|
||||
make build-wincred PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
mv /out/docker-credential-wincred /out/docker-credential-wincred.exe
|
||||
xx-verify /out/docker-credential-wincred.exe
|
||||
;;
|
||||
esac
|
||||
EOT
|
||||
|
||||
FROM base AS build-windows
|
||||
ARG PACKAGE
|
||||
RUN --mount=type=bind,target=. \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=bind,source=/tmp/.version,target=/tmp/.version,from=version \
|
||||
--mount=type=bind,source=/tmp/.revision,target=/tmp/.revision,from=version <<EOT
|
||||
set -ex
|
||||
xx-go --wrap
|
||||
make build-wincred PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
|
||||
mv /out/docker-credential-wincred /out/docker-credential-wincred.exe
|
||||
xx-verify /out/docker-credential-wincred.exe
|
||||
EOT
|
||||
|
||||
FROM build-$TARGETOS AS build
|
||||
|
||||
FROM scratch AS binaries
|
||||
COPY --from=build /out /
|
||||
|
||||
|
||||
@@ -7,6 +7,28 @@ GO_LDFLAGS = -s -w -X ${GO_PKG}/credentials.Version=${VERSION} -X ${GO_PKG}/cred
|
||||
|
||||
BUILDX_CMD ?= docker buildx
|
||||
DESTDIR ?= ./bin/build
|
||||
COVERAGEDIR ?= ./bin/coverage
|
||||
|
||||
# 10.11 is the minimum supported version for osxkeychain
|
||||
export MACOSX_DEPLOYMENT_TARGET = 10.11
|
||||
ifeq "$(shell go env GOOS)" "darwin"
|
||||
export CGO_CFLAGS = -Wno-atomic-alignment -mmacosx-version-min=$(MACOSX_DEPLOYMENT_TARGET)
|
||||
else
|
||||
# prevent warnings; see https://github.com/docker/docker-credential-helpers/pull/340#issuecomment-2437593837
|
||||
# gcc_libinit.c:44:8: error: large atomic operation may incur significant performance penalty; the access size (4 bytes) exceeds the max lock-free size (0 bytes) [-Werror,-Watomic-alignment]
|
||||
export CGO_CFLAGS = -Wno-atomic-alignment
|
||||
endif
|
||||
|
||||
ifeq "$(shell go env GOOS)/$(shell go env GOARCH)/$(shell go env GOARM)" "linux/arm/6"
|
||||
# Neither the CGo compiler, nor the C toolchain automatically link to
|
||||
# libatomic when the architecture doesn't support atomic intrinsics, as is
|
||||
# the case for arm/v6.
|
||||
#
|
||||
# Here's the error we get when this is not done (see https://github.com/docker/docker-credential-helpers/pull/340#issuecomment-2437593837):
|
||||
#
|
||||
# gcc_libinit.c:44:8: error: large atomic operation may incur significant performance penalty; the access size (4 bytes) exceeds the max lock-free size (0 bytes) [-Werror,-Watomic-alignment]
|
||||
export CGO_LDFLAGS=-latomic
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cross
|
||||
@@ -35,8 +57,10 @@ release: # create release
|
||||
./hack/release
|
||||
|
||||
.PHONY: test
|
||||
test: # tests all packages except vendor
|
||||
go test -v `go list ./... | grep -v /vendor/`
|
||||
test:
|
||||
mkdir -p $(COVERAGEDIR)
|
||||
go test -short -v -coverprofile=$(COVERAGEDIR)/coverage.txt -covermode=atomic ./...
|
||||
go tool cover -func=$(COVERAGEDIR)/coverage.txt
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@@ -72,3 +96,6 @@ vendor:
|
||||
rm -rf ./vendor
|
||||
cp -R "$($@_TMP_OUT)"/* .
|
||||
rm -rf "$($@_TMP_OUT)"
|
||||
|
||||
.PHONY: print-%
|
||||
print-%: ; @echo $($*)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[](https://github.com/docker/docker-credential-helpers/releases/latest)
|
||||
[](https://pkg.go.dev/github.com/docker/docker-credential-helpers)
|
||||
[](https://github.com/docker/docker-credential-helpers/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/docker/docker-credential-helpers/actions?query=workflow%3Abuild)
|
||||
[](https://codecov.io/gh/docker/docker-credential-helpers)
|
||||
[](https://goreportcard.com/report/github.com/docker/docker-credential-helpers)
|
||||
|
||||
@@ -17,6 +17,9 @@ Go to the [Releases](https://github.com/docker/docker-credential-helpers/release
|
||||
You can build the credential helpers using Docker:
|
||||
|
||||
```shell
|
||||
# install emulators
|
||||
$ docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
|
||||
# create builder
|
||||
$ docker buildx create --use
|
||||
|
||||
@@ -31,20 +34,20 @@ $ docker buildx bake
|
||||
|
||||
Or if the toolchain is already installed on your machine:
|
||||
|
||||
1 - Download the source.
|
||||
1. Download the source.
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/docker/docker-credential-helpers.git
|
||||
$ cd docker-credential-helpers
|
||||
```
|
||||
|
||||
2 - Use `make` to build the program you want. That will leave an executable in the `bin` directory inside the repository.
|
||||
2. Use `make` to build the program you want. That will leave an executable in the `bin` directory inside the repository.
|
||||
|
||||
```shell
|
||||
$ make osxkeychain
|
||||
```
|
||||
|
||||
3 - Put that binary in your `$PATH`, so Docker can find it.
|
||||
3. Put that binary in your `$PATH`, so Docker can find it.
|
||||
|
||||
```shell
|
||||
$ cp bin/build/docker-credential-osxkeychain /usr/local/bin/
|
||||
|
||||
+9
-16
@@ -16,17 +16,15 @@ func isValidCredsMessage(msg string) error {
|
||||
if credentials.IsCredentialsMissingServerURLMessage(msg) {
|
||||
return credentials.NewErrCredentialsMissingServerURL()
|
||||
}
|
||||
|
||||
if credentials.IsCredentialsMissingUsernameMessage(msg) {
|
||||
return credentials.NewErrCredentialsMissingUsername()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store uses an external program to save credentials.
|
||||
func Store(program ProgramFunc, creds *credentials.Credentials) error {
|
||||
cmd := program("store")
|
||||
cmd := program(credentials.ActionStore)
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
|
||||
@@ -36,13 +34,10 @@ func Store(program ProgramFunc, creds *credentials.Credentials) error {
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t := strings.TrimSpace(string(out))
|
||||
|
||||
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
|
||||
if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil {
|
||||
err = isValidErr
|
||||
}
|
||||
|
||||
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
|
||||
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -50,22 +45,20 @@ func Store(program ProgramFunc, creds *credentials.Credentials) error {
|
||||
|
||||
// Get executes an external program to get the credentials from a native store.
|
||||
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
|
||||
cmd := program("get")
|
||||
cmd := program(credentials.ActionGet)
|
||||
cmd.Input(strings.NewReader(serverURL))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t := strings.TrimSpace(string(out))
|
||||
|
||||
if credentials.IsErrCredentialsNotFoundMessage(t) {
|
||||
if credentials.IsErrCredentialsNotFoundMessage(string(out)) {
|
||||
return nil, credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
|
||||
if isValidErr := isValidCredsMessage(string(out)); isValidErr != nil {
|
||||
err = isValidErr
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
|
||||
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
resp := &credentials.Credentials{
|
||||
@@ -81,7 +74,7 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
|
||||
|
||||
// Erase executes a program to remove the server credentials from the native store.
|
||||
func Erase(program ProgramFunc, serverURL string) error {
|
||||
cmd := program("erase")
|
||||
cmd := program(credentials.ActionErase)
|
||||
cmd.Input(strings.NewReader(serverURL))
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
@@ -99,7 +92,7 @@ func Erase(program ProgramFunc, serverURL string) error {
|
||||
|
||||
// List executes a program to list server credentials in the native store.
|
||||
func List(program ProgramFunc) (map[string]string, error) {
|
||||
cmd := program("list")
|
||||
cmd := program(credentials.ActionList)
|
||||
cmd.Input(strings.NewReader("unused"))
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
|
||||
+35
-28
@@ -11,24 +11,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
validServerAddress = "https://index.docker.io/v1"
|
||||
validServerAddress = "https://registry.example.com/v1"
|
||||
validUsername = "linus"
|
||||
validServerAddress2 = "https://example.com:5002"
|
||||
invalidServerAddress = "https://foobar.example.com"
|
||||
missingCredsAddress = "https://missing.docker.io/v1"
|
||||
missingCredsAddress = "https://missing.example.com/v1"
|
||||
)
|
||||
|
||||
var errProgramExited = fmt.Errorf("exited 1")
|
||||
|
||||
// mockProgram simulates interactions between the docker client and a remote
|
||||
// credentials helper.
|
||||
// credentials-helper.
|
||||
// Unit tests inject this mocked command into the remote to control execution.
|
||||
type mockProgram struct {
|
||||
arg string
|
||||
input io.Reader
|
||||
}
|
||||
|
||||
// Output returns responses from the remote credentials helper.
|
||||
// Output returns responses from the remote credentials-helper.
|
||||
// It mocks those responses based in the input in the mock.
|
||||
func (m *mockProgram) Output() ([]byte, error) {
|
||||
in, err := io.ReadAll(m.input)
|
||||
@@ -80,7 +80,7 @@ func (m *mockProgram) Output() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
||||
}
|
||||
|
||||
// Input sets the input to send to a remote credentials helper.
|
||||
// Input sets the input to send to a remote credentials-helper.
|
||||
func (m *mockProgram) Input(in io.Reader) {
|
||||
m.input = in
|
||||
}
|
||||
@@ -92,16 +92,16 @@ func mockProgramFn(args ...string) Program {
|
||||
}
|
||||
|
||||
func ExampleStore() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
p := NewShellProgramFunc("docker-credential-pass")
|
||||
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: "https://example.com",
|
||||
Username: "calavera",
|
||||
ServerURL: "https://registry.example.com",
|
||||
Username: "exampleuser",
|
||||
Secret: "my super secret token",
|
||||
}
|
||||
|
||||
if err := Store(p, c); err != nil {
|
||||
fmt.Println(err)
|
||||
_, _ = fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func TestStore(t *testing.T) {
|
||||
|
||||
for _, v := range valid {
|
||||
if err := Store(mockProgramFn, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,20 +123,20 @@ func TestStore(t *testing.T) {
|
||||
|
||||
for _, v := range invalid {
|
||||
if err := Store(mockProgramFn, &v); err == nil {
|
||||
t.Fatalf("Expected error for server %s, got nil", v.ServerURL)
|
||||
t.Errorf("Expected error for server %s, got nil", v.ServerURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleGet() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
p := NewShellProgramFunc("docker-credential-pass")
|
||||
|
||||
creds, err := Get(p, "https://example.com")
|
||||
creds, err := Get(p, "https://registry.example.com")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
_, _ = fmt.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Got credentials for user `%s` in `%s`\n", creds.Username, creds.ServerURL)
|
||||
_, _ = fmt.Printf("Got credentials for user `%s` in `%s`\n", creds.Username, creds.ServerURL)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
@@ -152,10 +152,10 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
if c.Username != v.Username {
|
||||
t.Fatalf("expected username `%s`, got %s", v.Username, c.Username)
|
||||
t.Errorf("expected username `%s`, got %s", v.Username, c.Username)
|
||||
}
|
||||
if c.Secret != v.Secret {
|
||||
t.Fatalf("expected secret `%s`, got %s", v.Secret, c.Secret)
|
||||
t.Errorf("expected secret `%s`, got %s", v.Secret, c.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,10 +165,17 @@ func TestGet(t *testing.T) {
|
||||
serverURL string
|
||||
err string
|
||||
}{
|
||||
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
|
||||
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
|
||||
{"", fmt.Sprintf("error getting credentials - err: %s, out: `%s`",
|
||||
missingServerURLErr.Error(), missingServerURLErr.Error())},
|
||||
{
|
||||
serverURL: missingCredsAddress,
|
||||
err: credentials.NewErrCredentialsNotFound().Error(),
|
||||
},
|
||||
{
|
||||
serverURL: invalidServerAddress,
|
||||
err: "error getting credentials - err: exited 1, out: `program failed`",
|
||||
},
|
||||
{
|
||||
err: fmt.Sprintf("error getting credentials - err: %s, out: `%s`", missingServerURLErr.Error(), missingServerURLErr.Error()),
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range invalid {
|
||||
@@ -177,26 +184,26 @@ func TestGet(t *testing.T) {
|
||||
t.Fatalf("Expected error for server %s, got nil", v.serverURL)
|
||||
}
|
||||
if err.Error() != v.err {
|
||||
t.Fatalf("Expected error `%s`, got `%v`", v.err, err)
|
||||
t.Errorf("Expected error `%s`, got `%v`", v.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleErase() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
p := NewShellProgramFunc("docker-credential-pass")
|
||||
|
||||
if err := Erase(p, "https://example.com"); err != nil {
|
||||
fmt.Println(err)
|
||||
if err := Erase(p, "https://registry.example.com"); err != nil {
|
||||
_, _ = fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErase(t *testing.T) {
|
||||
if err := Erase(mockProgramFn, validServerAddress); err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := Erase(mockProgramFn, invalidServerAddress); err == nil {
|
||||
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
||||
t.Errorf("Expected error for server %s, got nil", invalidServerAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +214,6 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
if username, exists := auths[validServerAddress]; !exists || username != validUsername {
|
||||
t.Fatalf("auths[%s] returned %s, %t; expected %s, %t", validServerAddress, username, exists, validUsername, true)
|
||||
t.Errorf("auths[%s] returned %s, %t; expected %s, %t", validServerAddress, username, exists, validUsername, true)
|
||||
}
|
||||
}
|
||||
|
||||
+20
-20
@@ -1,11 +1,9 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
exec "golang.org/x/sys/execabs"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Program is an interface to execute external programs.
|
||||
@@ -17,41 +15,43 @@ type Program interface {
|
||||
// ProgramFunc is a type of function that initializes programs based on arguments.
|
||||
type ProgramFunc func(args ...string) Program
|
||||
|
||||
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||
func NewShellProgramFunc(name string) ProgramFunc {
|
||||
return NewShellProgramFuncWithEnv(name, nil)
|
||||
}
|
||||
|
||||
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
|
||||
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
|
||||
// NewShellProgramFunc creates a [ProgramFunc] to run command in a [Shell].
|
||||
func NewShellProgramFunc(command string) ProgramFunc {
|
||||
return func(args ...string) Program {
|
||||
return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
|
||||
return createProgramCmdRedirectErr(command, args, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
|
||||
programCmd := exec.Command(commandName, args...)
|
||||
programCmd.Env = os.Environ()
|
||||
// NewShellProgramFuncWithEnv creates a [ProgramFunc] tu run command
|
||||
// in a [Shell] with the given environment variables.
|
||||
func NewShellProgramFuncWithEnv(command string, env *map[string]string) ProgramFunc {
|
||||
return func(args ...string) Program {
|
||||
return createProgramCmdRedirectErr(command, args, env)
|
||||
}
|
||||
}
|
||||
|
||||
func createProgramCmdRedirectErr(command string, args []string, env *map[string]string) *Shell {
|
||||
ec := exec.Command(command, args...)
|
||||
if env != nil {
|
||||
for k, v := range *env {
|
||||
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
ec.Env = append(ec.Environ(), k+"="+v)
|
||||
}
|
||||
}
|
||||
programCmd.Stderr = os.Stderr
|
||||
return programCmd
|
||||
ec.Stderr = os.Stderr
|
||||
return &Shell{cmd: ec}
|
||||
}
|
||||
|
||||
// Shell invokes shell commands to talk with a remote credentials helper.
|
||||
// Shell invokes shell commands to talk with a remote credentials-helper.
|
||||
type Shell struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Output returns responses from the remote credentials helper.
|
||||
// Output returns responses from the remote credentials-helper.
|
||||
func (s *Shell) Output() ([]byte, error) {
|
||||
return s.cmd.Output()
|
||||
}
|
||||
|
||||
// Input sets the input to send to a remote credentials helper.
|
||||
// Input sets the input to send to a remote credentials-helper.
|
||||
func (s *Shell) Input(in io.Reader) {
|
||||
s.cmd.Stdin = in
|
||||
}
|
||||
|
||||
+46
-23
@@ -10,6 +10,20 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Action defines the name of an action (sub-command) supported by a
|
||||
// credential-helper binary. It is an alias for "string", and mostly
|
||||
// for convenience.
|
||||
type Action = string
|
||||
|
||||
// List of actions (sub-commands) supported by credential-helper binaries.
|
||||
const (
|
||||
ActionStore Action = "store"
|
||||
ActionGet Action = "get"
|
||||
ActionErase Action = "erase"
|
||||
ActionList Action = "list"
|
||||
ActionVersion Action = "version"
|
||||
)
|
||||
|
||||
// Credentials holds the information shared between docker and the credentials store.
|
||||
type Credentials struct {
|
||||
ServerURL string
|
||||
@@ -43,42 +57,52 @@ func SetCredsLabel(label string) {
|
||||
CredsLabel = label
|
||||
}
|
||||
|
||||
// Serve initializes the credentials helper and parses the action argument.
|
||||
// Serve initializes the credentials-helper and parses the action argument.
|
||||
// This function is designed to be called from a command line interface.
|
||||
// It uses os.Args[1] as the key for the action.
|
||||
// It uses os.Stdin as input and os.Stdout as output.
|
||||
// This function terminates the program with os.Exit(1) if there is an error.
|
||||
func Serve(helper Helper) {
|
||||
var err error
|
||||
if len(os.Args) != 2 {
|
||||
err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
|
||||
_, _ = fmt.Fprintln(os.Stdout, usage())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout)
|
||||
switch os.Args[1] {
|
||||
case "--version", "-v":
|
||||
_ = PrintVersion(os.Stdout)
|
||||
os.Exit(0)
|
||||
case "--help", "-h":
|
||||
_, _ = fmt.Fprintln(os.Stdout, usage())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stdout, "%v\n", err)
|
||||
if err := HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stdout, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommand uses a helper and a key to run a credential action.
|
||||
func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error {
|
||||
switch key {
|
||||
case "store":
|
||||
func usage() string {
|
||||
return fmt.Sprintf("Usage: %s <store|get|erase|list|version>", Name)
|
||||
}
|
||||
|
||||
// HandleCommand runs a helper to execute a credential action.
|
||||
func HandleCommand(helper Helper, action Action, in io.Reader, out io.Writer) error {
|
||||
switch action {
|
||||
case ActionStore:
|
||||
return Store(helper, in)
|
||||
case "get":
|
||||
case ActionGet:
|
||||
return Get(helper, in, out)
|
||||
case "erase":
|
||||
case ActionErase:
|
||||
return Erase(helper, in)
|
||||
case "list":
|
||||
case ActionList:
|
||||
return List(helper, out)
|
||||
case "version":
|
||||
case ActionVersion:
|
||||
return PrintVersion(out)
|
||||
default:
|
||||
return fmt.Errorf("%s: unknown action: %s", Name, action)
|
||||
}
|
||||
return fmt.Errorf("Unknown credential action `%s`", key)
|
||||
}
|
||||
|
||||
// Store uses a helper and an input reader to save credentials.
|
||||
@@ -132,18 +156,17 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
resp := Credentials{
|
||||
buffer.Reset()
|
||||
err = json.NewEncoder(buffer).Encode(Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: username,
|
||||
Secret: secret,
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
if err := json.NewEncoder(buffer).Encode(resp); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(writer, buffer.String())
|
||||
_, _ = fmt.Fprint(writer, buffer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,6 +204,6 @@ func List(helper Helper, writer io.Writer) error {
|
||||
|
||||
// PrintVersion outputs the current version.
|
||||
func PrintVersion(writer io.Writer) error {
|
||||
fmt.Fprintf(writer, "%s (%s) %s\n", Name, Package, Version)
|
||||
_, _ = fmt.Fprintf(writer, "%s (%s) %s\n", Name, Package, Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,12 +37,12 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
|
||||
}
|
||||
|
||||
func (m *memoryStore) List() (map[string]string, error) {
|
||||
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
|
||||
// Simply a placeholder to let memoryStore be a valid implementation of Helper interface
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
serverURL := "https://index.docker.io/v1/"
|
||||
const serverURL = "https://registry.example.com/v1/"
|
||||
creds := &Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: "foo",
|
||||
@@ -65,11 +65,11 @@ func TestStore(t *testing.T) {
|
||||
}
|
||||
|
||||
if c.Username != "foo" {
|
||||
t.Fatalf("expected username foo, got %s\n", c.Username)
|
||||
t.Errorf("expected username foo, got %s\n", c.Username)
|
||||
}
|
||||
|
||||
if c.Secret != "bar" {
|
||||
t.Fatalf("expected username bar, got %s\n", c.Secret)
|
||||
t.Errorf("expected username bar, got %s\n", c.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ func TestStoreMissingServerURL(t *testing.T) {
|
||||
|
||||
h := newMemoryStore()
|
||||
|
||||
if err := Store(h, in); IsCredentialsMissingServerURL(err) == false {
|
||||
t.Fatal(err)
|
||||
if err := Store(h, in); !IsCredentialsMissingServerURL(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreMissingUsername(t *testing.T) {
|
||||
creds := &Credentials{
|
||||
ServerURL: "https://index.docker.io/v1/",
|
||||
ServerURL: "https://registry.example.com/v1/",
|
||||
Username: "",
|
||||
Secret: "bar",
|
||||
}
|
||||
@@ -108,13 +108,13 @@ func TestStoreMissingUsername(t *testing.T) {
|
||||
|
||||
h := newMemoryStore()
|
||||
|
||||
if err := Store(h, in); IsCredentialsMissingUsername(err) == false {
|
||||
t.Fatal(err)
|
||||
if err := Store(h, in); !IsCredentialsMissingUsername(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
serverURL := "https://index.docker.io/v1/"
|
||||
const serverURL = "https://registry.example.com/v1/"
|
||||
creds := &Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: "foo",
|
||||
@@ -147,16 +147,16 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
if c.Username != "foo" {
|
||||
t.Fatalf("expected username foo, got %s\n", c.Username)
|
||||
t.Errorf("expected username foo, got %s\n", c.Username)
|
||||
}
|
||||
|
||||
if c.Secret != "bar" {
|
||||
t.Fatalf("expected username bar, got %s\n", c.Secret)
|
||||
t.Errorf("expected username bar, got %s\n", c.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMissingServerURL(t *testing.T) {
|
||||
serverURL := "https://index.docker.io/v1/"
|
||||
const serverURL = "https://registry.example.com/v1/"
|
||||
creds := &Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: "foo",
|
||||
@@ -176,13 +176,13 @@ func TestGetMissingServerURL(t *testing.T) {
|
||||
buf := strings.NewReader("")
|
||||
w := new(bytes.Buffer)
|
||||
|
||||
if err := Get(h, buf, w); IsCredentialsMissingServerURL(err) == false {
|
||||
t.Fatal(err)
|
||||
if err := Get(h, buf, w); !IsCredentialsMissingServerURL(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErase(t *testing.T) {
|
||||
serverURL := "https://index.docker.io/v1/"
|
||||
const serverURL = "https://registry.example.com/v1/"
|
||||
creds := &Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: "foo",
|
||||
@@ -206,12 +206,12 @@ func TestErase(t *testing.T) {
|
||||
|
||||
w := new(bytes.Buffer)
|
||||
if err := Get(h, buf, w); err == nil {
|
||||
t.Fatal("expected error getting missing creds, got empty")
|
||||
t.Error("expected error getting missing creds, got empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEraseMissingServerURL(t *testing.T) {
|
||||
serverURL := "https://index.docker.io/v1/"
|
||||
const serverURL = "https://registry.example.com/v1/"
|
||||
creds := &Credentials{
|
||||
ServerURL: serverURL,
|
||||
Username: "foo",
|
||||
@@ -229,21 +229,21 @@ func TestEraseMissingServerURL(t *testing.T) {
|
||||
}
|
||||
|
||||
buf := strings.NewReader("")
|
||||
if err := Erase(h, buf); IsCredentialsMissingServerURL(err) == false {
|
||||
t.Fatal(err)
|
||||
if err := Erase(h, buf); !IsCredentialsMissingServerURL(err) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
//This tests that there is proper input an output into the byte stream
|
||||
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
|
||||
// This tests that there is proper input an output into the byte stream
|
||||
// Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
|
||||
out := new(bytes.Buffer)
|
||||
h := newMemoryStore()
|
||||
if err := List(h, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
//testing that there is an output
|
||||
// testing that there is an output
|
||||
if out.Len() == 0 {
|
||||
t.Fatalf("expected output in the writer, got %d", 0)
|
||||
t.Error("expected output in the writer, got 0")
|
||||
}
|
||||
}
|
||||
|
||||
+31
-9
@@ -1,5 +1,10 @@
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
|
||||
// the same message and docker can handle it properly.
|
||||
@@ -21,6 +26,11 @@ func (errCredentialsNotFound) Error() string {
|
||||
return errCredentialsNotFoundMessage
|
||||
}
|
||||
|
||||
// NotFound implements the [ErrNotFound][errdefs.ErrNotFound] interface.
|
||||
//
|
||||
// [errdefs.ErrNotFound]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrNotFound
|
||||
func (errCredentialsNotFound) NotFound() {}
|
||||
|
||||
// NewErrCredentialsNotFound creates a new error
|
||||
// for when the credentials are not in the store.
|
||||
func NewErrCredentialsNotFound() error {
|
||||
@@ -30,8 +40,8 @@ func NewErrCredentialsNotFound() error {
|
||||
// IsErrCredentialsNotFound returns true if the error
|
||||
// was caused by not having a set of credentials in a store.
|
||||
func IsErrCredentialsNotFound(err error) bool {
|
||||
_, ok := err.(errCredentialsNotFound)
|
||||
return ok
|
||||
var target errCredentialsNotFound
|
||||
return errors.As(err, &target)
|
||||
}
|
||||
|
||||
// IsErrCredentialsNotFoundMessage returns true if the error
|
||||
@@ -40,7 +50,7 @@ func IsErrCredentialsNotFound(err error) bool {
|
||||
// This function helps to check messages returned by an
|
||||
// external program via its standard output.
|
||||
func IsErrCredentialsNotFoundMessage(err string) bool {
|
||||
return err == errCredentialsNotFoundMessage
|
||||
return strings.TrimSpace(err) == errCredentialsNotFoundMessage
|
||||
}
|
||||
|
||||
// errCredentialsMissingServerURL represents an error raised
|
||||
@@ -53,6 +63,12 @@ func (errCredentialsMissingServerURL) Error() string {
|
||||
return errCredentialsMissingServerURLMessage
|
||||
}
|
||||
|
||||
// InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter]
|
||||
// interface.
|
||||
//
|
||||
// [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter
|
||||
func (errCredentialsMissingServerURL) InvalidParameter() {}
|
||||
|
||||
// errCredentialsMissingUsername represents an error raised
|
||||
// when the credentials object has no username or when no
|
||||
// username is provided to a credentials operation requiring
|
||||
@@ -63,6 +79,12 @@ func (errCredentialsMissingUsername) Error() string {
|
||||
return errCredentialsMissingUsernameMessage
|
||||
}
|
||||
|
||||
// InvalidParameter implements the [ErrInvalidParameter][errdefs.ErrInvalidParameter]
|
||||
// interface.
|
||||
//
|
||||
// [errdefs.ErrInvalidParameter]: https://pkg.go.dev/github.com/docker/docker@v24.0.1+incompatible/errdefs#ErrInvalidParameter
|
||||
func (errCredentialsMissingUsername) InvalidParameter() {}
|
||||
|
||||
// NewErrCredentialsMissingServerURL creates a new error for
|
||||
// errCredentialsMissingServerURL.
|
||||
func NewErrCredentialsMissingServerURL() error {
|
||||
@@ -78,25 +100,25 @@ func NewErrCredentialsMissingUsername() error {
|
||||
// IsCredentialsMissingServerURL returns true if the error
|
||||
// was an errCredentialsMissingServerURL.
|
||||
func IsCredentialsMissingServerURL(err error) bool {
|
||||
_, ok := err.(errCredentialsMissingServerURL)
|
||||
return ok
|
||||
var target errCredentialsMissingServerURL
|
||||
return errors.As(err, &target)
|
||||
}
|
||||
|
||||
// IsCredentialsMissingServerURLMessage checks for an
|
||||
// errCredentialsMissingServerURL in the error message.
|
||||
func IsCredentialsMissingServerURLMessage(err string) bool {
|
||||
return err == errCredentialsMissingServerURLMessage
|
||||
return strings.TrimSpace(err) == errCredentialsMissingServerURLMessage
|
||||
}
|
||||
|
||||
// IsCredentialsMissingUsername returns true if the error
|
||||
// was an errCredentialsMissingUsername.
|
||||
func IsCredentialsMissingUsername(err error) bool {
|
||||
_, ok := err.(errCredentialsMissingUsername)
|
||||
return ok
|
||||
var target errCredentialsMissingUsername
|
||||
return errors.As(err, &target)
|
||||
}
|
||||
|
||||
// IsCredentialsMissingUsernameMessage checks for an
|
||||
// errCredentialsMissingUsername in the error message.
|
||||
func IsCredentialsMissingUsernameMessage(err string) bool {
|
||||
return err == errCredentialsMissingUsernameMessage
|
||||
return strings.TrimSpace(err) == errCredentialsMissingUsernameMessage
|
||||
}
|
||||
|
||||
+14
-7
@@ -1,12 +1,19 @@
|
||||
ARG GO_VERSION=1.18.5
|
||||
ARG DISTRO=ubuntu
|
||||
ARG SUITE=focal
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM golang:${GO_VERSION}-buster AS golang
|
||||
# GO_VERSION sets the version of the golang base image to use.
|
||||
# It must be a valid tag in the docker.io/library/golang image repository.
|
||||
ARG GO_VERSION=1.25.9
|
||||
|
||||
# BASE_DEBIAN_DISTRO sets the golang base image debian variant to use.
|
||||
# It must be a valid variant in the docker.io/library/golang image repository.
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG DISTRO=ubuntu
|
||||
ARG SUITE=jammy
|
||||
|
||||
FROM golang:${GO_VERSION}-${BASE_DEBIAN_DISTRO} AS gobase
|
||||
|
||||
FROM ${DISTRO}:${SUITE}
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -yy debhelper dh-make libsecret-1-dev
|
||||
RUN mkdir -p /build
|
||||
|
||||
@@ -15,7 +22,7 @@ ENV GOPROXY=https://proxy.golang.org|direct
|
||||
ENV GO111MODULE=off
|
||||
ENV GOPATH=/build
|
||||
ENV PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
|
||||
COPY --from=golang /usr/local/go /usr/local/go
|
||||
COPY --from=gobase /usr/local/go /usr/local/go
|
||||
|
||||
COPY Makefile .
|
||||
COPY credentials credentials
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.18.5"
|
||||
default = null
|
||||
}
|
||||
|
||||
# Defines the output folder
|
||||
@@ -62,7 +62,8 @@ target "binaries" {
|
||||
"linux/arm/v6",
|
||||
"linux/ppc64le",
|
||||
"linux/s390x",
|
||||
"windows/amd64"
|
||||
"windows/amd64",
|
||||
"windows/arm64"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
module github.com/docker/docker-credential-helpers
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
retract (
|
||||
v0.9.1 // osxkeychain: a regression caused backward-incompatibility with earlier versions
|
||||
v0.9.0 // osxkeychain: a regression caused backward-incompatibility with earlier versions
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/danieljoos/wincred v1.1.2
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
|
||||
github.com/danieljoos/wincred v1.2.3
|
||||
github.com/keybase/go-keychain v0.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.20.0 // indirect
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
|
||||
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
|
||||
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
|
||||
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -0,0 +1,182 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package osxkeychain
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework Security -framework CoreFoundation
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/docker/docker-credential-helpers/registryurl"
|
||||
"github.com/keybase/go-keychain"
|
||||
)
|
||||
|
||||
// https://opensource.apple.com/source/Security/Security-55471/sec/Security/SecBase.h.auto.html
|
||||
const (
|
||||
// errCredentialsNotFound is the specific error message returned by OS X
|
||||
// when the credentials are not in the keychain.
|
||||
errCredentialsNotFound = "The specified item could not be found in the keychain. (-25300)"
|
||||
// errInteractionNotAllowed is the specific error message returned by OS X
|
||||
// when environment does not allow showing dialog to unlock keychain.
|
||||
errInteractionNotAllowed = "User interaction is not allowed. (-25308)"
|
||||
)
|
||||
|
||||
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown.
|
||||
var ErrInteractionNotAllowed = errors.New(`keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again`)
|
||||
|
||||
// Osxkeychain handles secrets using the OS X Keychain as store.
|
||||
type Osxkeychain struct{}
|
||||
|
||||
// Add adds new credentials to the keychain.
|
||||
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
||||
_ = h.Delete(creds.ServerURL) // ignore errors as existing credential may not exist.
|
||||
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassInternetPassword)
|
||||
item.SetLabel(credentials.CredsLabel)
|
||||
item.SetAccount(creds.Username)
|
||||
item.SetData([]byte(creds.Secret))
|
||||
// Prior to v0.9, the credential helper was searching for credentials with
|
||||
// the "dflt" authentication type (see [1]). Since v0.9.0, Get doesn't use
|
||||
// that attribute anymore, and v0.9.0 - v0.9.2 were not setting it here
|
||||
// either.
|
||||
//
|
||||
// In order to keep compatibility with older versions, we need to store
|
||||
// credentials with this attribute set. This way, credentials stored with
|
||||
// newer versions can be retrieved by older versions.
|
||||
//
|
||||
// [1]: https://github.com/docker/docker-credential-helpers/blob/v0.8.2/osxkeychain/osxkeychain.c#L66
|
||||
item.SetAuthenticationType("dflt")
|
||||
if err := splitServer(creds.ServerURL, item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return keychain.AddItem(item)
|
||||
}
|
||||
|
||||
// Delete removes credentials from the keychain.
|
||||
func (h Osxkeychain) Delete(serverURL string) error {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassInternetPassword)
|
||||
if err := splitServer(serverURL, item); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := keychain.DeleteItem(item); err != nil {
|
||||
switch err.Error() {
|
||||
case errCredentialsNotFound:
|
||||
return credentials.NewErrCredentialsNotFound()
|
||||
case errInteractionNotAllowed:
|
||||
return ErrInteractionNotAllowed
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the username and secret to use for a given registry server URL.
|
||||
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassInternetPassword)
|
||||
item.SetMatchLimit(keychain.MatchLimitOne)
|
||||
item.SetReturnAttributes(true)
|
||||
item.SetReturnData(true)
|
||||
if err := splitServer(serverURL, item); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
res, err := keychain.QueryItem(item)
|
||||
if err != nil {
|
||||
switch err.Error() {
|
||||
case errCredentialsNotFound:
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
case errInteractionNotAllowed:
|
||||
return "", "", ErrInteractionNotAllowed
|
||||
default:
|
||||
return "", "", err
|
||||
}
|
||||
} else if len(res) == 0 {
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
return res[0].Account, string(res[0].Data), nil
|
||||
}
|
||||
|
||||
// List returns the stored URLs and corresponding usernames.
|
||||
func (h Osxkeychain) List() (map[string]string, error) {
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassInternetPassword)
|
||||
item.SetMatchLimit(keychain.MatchLimitAll)
|
||||
item.SetReturnAttributes(true)
|
||||
item.SetLabel(credentials.CredsLabel)
|
||||
|
||||
res, err := keychain.QueryItem(item)
|
||||
if err != nil {
|
||||
switch err.Error() {
|
||||
case errCredentialsNotFound:
|
||||
return make(map[string]string), nil
|
||||
case errInteractionNotAllowed:
|
||||
return nil, ErrInteractionNotAllowed
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp := make(map[string]string)
|
||||
for _, r := range res {
|
||||
proto := "http"
|
||||
if r.Protocol == kSecProtocolTypeHTTPS {
|
||||
proto = "https"
|
||||
}
|
||||
host := r.Server
|
||||
if r.Port != 0 {
|
||||
host = net.JoinHostPort(host, strconv.Itoa(int(r.Port)))
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: proto,
|
||||
Host: host,
|
||||
Path: r.Path,
|
||||
}
|
||||
resp[u.String()] = r.Account
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// Hardcoded protocol types matching their Objective-C equivalents.
|
||||
// https://developer.apple.com/documentation/security/ksecattrprotocolhttps?language=objc
|
||||
kSecProtocolTypeHTTPS = "htps" // This is NOT a typo.
|
||||
// https://developer.apple.com/documentation/security/ksecattrprotocolhttp?language=objc
|
||||
kSecProtocolTypeHTTP = "http"
|
||||
)
|
||||
|
||||
func splitServer(serverURL string, item keychain.Item) error {
|
||||
u, err := registryurl.Parse(serverURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.SetProtocol(kSecProtocolTypeHTTPS)
|
||||
if u.Scheme == "http" {
|
||||
item.SetProtocol(kSecProtocolTypeHTTP)
|
||||
}
|
||||
item.SetServer(u.Hostname())
|
||||
if p := u.Port(); p != "" {
|
||||
port, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.SetPort(int32(port))
|
||||
}
|
||||
item.SetPath(u.Path)
|
||||
return nil
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
#include "osxkeychain_darwin.h"
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Foundation/NSValue.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
char *get_error(OSStatus status) {
|
||||
char *buf = malloc(128);
|
||||
CFStringRef str = SecCopyErrorMessageString(status, NULL);
|
||||
int success = CFStringGetCString(str, buf, 128, kCFStringEncodingUTF8);
|
||||
if (!success) {
|
||||
strncpy(buf, "Unknown error", 128);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
char *keychain_add(struct Server *server, char *label, char *username, char *secret) {
|
||||
SecKeychainItemRef item;
|
||||
|
||||
OSStatus status = SecKeychainAddInternetPassword(
|
||||
NULL,
|
||||
strlen(server->host), server->host,
|
||||
0, NULL,
|
||||
strlen(username), username,
|
||||
strlen(server->path), server->path,
|
||||
server->port,
|
||||
server->proto,
|
||||
kSecAuthenticationTypeDefault,
|
||||
strlen(secret), secret,
|
||||
&item
|
||||
);
|
||||
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
|
||||
SecKeychainAttribute attribute;
|
||||
SecKeychainAttributeList attrs;
|
||||
attribute.tag = kSecLabelItemAttr;
|
||||
attribute.data = label;
|
||||
attribute.length = strlen(label);
|
||||
attrs.count = 1;
|
||||
attrs.attr = &attribute;
|
||||
|
||||
status = SecKeychainItemModifyContent(item, &attrs, 0, NULL);
|
||||
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret) {
|
||||
char *tmp;
|
||||
SecKeychainItemRef item;
|
||||
|
||||
OSStatus status = SecKeychainFindInternetPassword(
|
||||
NULL,
|
||||
strlen(server->host), server->host,
|
||||
0, NULL,
|
||||
0, NULL,
|
||||
strlen(server->path), server->path,
|
||||
server->port,
|
||||
server->proto,
|
||||
kSecAuthenticationTypeDefault,
|
||||
secret_l, (void **)&tmp,
|
||||
&item);
|
||||
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
|
||||
*secret = strdup(tmp);
|
||||
SecKeychainItemFreeContent(NULL, tmp);
|
||||
|
||||
SecKeychainAttributeList list;
|
||||
SecKeychainAttribute attr;
|
||||
|
||||
list.count = 1;
|
||||
list.attr = &attr;
|
||||
attr.tag = kSecAccountItemAttr;
|
||||
|
||||
status = SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL);
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
|
||||
*username = strdup(attr.data);
|
||||
*username_l = attr.length;
|
||||
SecKeychainItemFreeContent(&list, NULL);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *keychain_delete(struct Server *server) {
|
||||
SecKeychainItemRef item;
|
||||
|
||||
OSStatus status = SecKeychainFindInternetPassword(
|
||||
NULL,
|
||||
strlen(server->host), server->host,
|
||||
0, NULL,
|
||||
0, NULL,
|
||||
strlen(server->path), server->path,
|
||||
server->port,
|
||||
server->proto,
|
||||
kSecAuthenticationTypeDefault,
|
||||
0, NULL,
|
||||
&item);
|
||||
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
|
||||
status = SecKeychainItemDelete(item);
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char * CFStringToCharArr(CFStringRef aString) {
|
||||
if (aString == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
CFIndex length = CFStringGetLength(aString);
|
||||
CFIndex maxSize =
|
||||
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
char *buffer = (char *)malloc(maxSize);
|
||||
if (CFStringGetCString(aString, buffer, maxSize,
|
||||
kCFStringEncodingUTF8)) {
|
||||
return buffer;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *keychain_list(char *credsLabel, char *** paths, char *** accts, unsigned int *list_l) {
|
||||
CFStringRef credsLabelCF = CFStringCreateWithCString(NULL, credsLabel, kCFStringEncodingUTF8);
|
||||
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
|
||||
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
|
||||
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
|
||||
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
|
||||
CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF);
|
||||
//Use this query dictionary
|
||||
CFTypeRef result= NULL;
|
||||
OSStatus status = SecItemCopyMatching(
|
||||
query,
|
||||
&result);
|
||||
|
||||
CFRelease(credsLabelCF);
|
||||
|
||||
//Ran a search and store the results in result
|
||||
if (status) {
|
||||
return get_error(status);
|
||||
}
|
||||
CFIndex numKeys = CFArrayGetCount(result);
|
||||
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||
//result is of type CFArray
|
||||
for(CFIndex i=0; i<numKeys; i++) {
|
||||
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
|
||||
|
||||
CFStringRef protocolTmp = CFDictionaryGetValue(currKey, CFSTR("ptcl"));
|
||||
if (protocolTmp != NULL) {
|
||||
CFStringRef protocolStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), protocolTmp);
|
||||
if (CFStringCompare(protocolStr, CFSTR("htps"), 0) == kCFCompareEqualTo) {
|
||||
protocolTmp = CFSTR("https://");
|
||||
}
|
||||
else {
|
||||
protocolTmp = CFSTR("http://");
|
||||
}
|
||||
CFRelease(protocolStr);
|
||||
}
|
||||
else {
|
||||
char * path = "0";
|
||||
char * acct = "0";
|
||||
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
|
||||
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
|
||||
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
|
||||
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
|
||||
continue;
|
||||
}
|
||||
|
||||
CFMutableStringRef str = CFStringCreateMutableCopy(NULL, 0, protocolTmp);
|
||||
CFStringRef serverTmp = CFDictionaryGetValue(currKey, CFSTR("srvr"));
|
||||
if (serverTmp != NULL) {
|
||||
CFStringAppend(str, serverTmp);
|
||||
}
|
||||
|
||||
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
|
||||
if (pathTmp != NULL) {
|
||||
CFStringAppend(str, pathTmp);
|
||||
}
|
||||
|
||||
const NSNumber * portTmp = CFDictionaryGetValue(currKey, CFSTR("port"));
|
||||
if (portTmp != NULL && portTmp.integerValue != 0) {
|
||||
CFStringRef portStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), portTmp);
|
||||
CFStringAppend(str, CFSTR(":"));
|
||||
CFStringAppend(str, portStr);
|
||||
CFRelease(portStr);
|
||||
}
|
||||
|
||||
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
|
||||
if (acctTmp == NULL) {
|
||||
acctTmp = CFSTR("account not defined");
|
||||
}
|
||||
|
||||
char * path = CFStringToCharArr(str);
|
||||
char * acct = CFStringToCharArr(acctTmp);
|
||||
|
||||
//We now have all we need, username and servername. Now export this to .go
|
||||
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
|
||||
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
|
||||
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
|
||||
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
|
||||
|
||||
CFRelease(str);
|
||||
}
|
||||
*list_l = (int)numKeys;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void freeListData(char *** data, unsigned int length) {
|
||||
for(int i=0; i<length; i++) {
|
||||
free((*data)[i]);
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package osxkeychain
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.11
|
||||
#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.11
|
||||
|
||||
#include "osxkeychain_darwin.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/docker/docker-credential-helpers/registryurl"
|
||||
)
|
||||
|
||||
// errCredentialsNotFound is the specific error message returned by OS X
|
||||
// when the credentials are not in the keychain.
|
||||
const errCredentialsNotFound = "The specified item could not be found in the keychain."
|
||||
|
||||
// errCredentialsNotFound is the specific error message returned by OS X
|
||||
// when environment does not allow showing dialog to unlock keychain.
|
||||
const errInteractionNotAllowed = "User interaction is not allowed."
|
||||
|
||||
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown.
|
||||
var ErrInteractionNotAllowed = errors.New(`keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again`)
|
||||
|
||||
// Osxkeychain handles secrets using the OS X Keychain as store.
|
||||
type Osxkeychain struct{}
|
||||
|
||||
// Add adds new credentials to the keychain.
|
||||
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
||||
h.Delete(creds.ServerURL)
|
||||
|
||||
s, err := splitServer(creds.ServerURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer freeServer(s)
|
||||
|
||||
label := C.CString(credentials.CredsLabel)
|
||||
defer C.free(unsafe.Pointer(label))
|
||||
username := C.CString(creds.Username)
|
||||
defer C.free(unsafe.Pointer(username))
|
||||
secret := C.CString(creds.Secret)
|
||||
defer C.free(unsafe.Pointer(secret))
|
||||
|
||||
errMsg := C.keychain_add(s, label, username, secret)
|
||||
if errMsg != nil {
|
||||
defer C.free(unsafe.Pointer(errMsg))
|
||||
return errors.New(C.GoString(errMsg))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes credentials from the keychain.
|
||||
func (h Osxkeychain) Delete(serverURL string) error {
|
||||
s, err := splitServer(serverURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer freeServer(s)
|
||||
|
||||
errMsg := C.keychain_delete(s)
|
||||
if errMsg != nil {
|
||||
defer C.free(unsafe.Pointer(errMsg))
|
||||
return errors.New(C.GoString(errMsg))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the username and secret to use for a given registry server URL.
|
||||
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
||||
s, err := splitServer(serverURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer freeServer(s)
|
||||
|
||||
var usernameLen C.uint
|
||||
var username *C.char
|
||||
var secretLen C.uint
|
||||
var secret *C.char
|
||||
defer C.free(unsafe.Pointer(username))
|
||||
defer C.free(unsafe.Pointer(secret))
|
||||
|
||||
errMsg := C.keychain_get(s, &usernameLen, &username, &secretLen, &secret)
|
||||
if errMsg != nil {
|
||||
defer C.free(unsafe.Pointer(errMsg))
|
||||
goMsg := C.GoString(errMsg)
|
||||
if goMsg == errCredentialsNotFound {
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
if goMsg == errInteractionNotAllowed {
|
||||
return "", "", ErrInteractionNotAllowed
|
||||
}
|
||||
|
||||
return "", "", errors.New(goMsg)
|
||||
}
|
||||
|
||||
user := C.GoStringN(username, C.int(usernameLen))
|
||||
pass := C.GoStringN(secret, C.int(secretLen))
|
||||
return user, pass, nil
|
||||
}
|
||||
|
||||
// List returns the stored URLs and corresponding usernames.
|
||||
func (h Osxkeychain) List() (map[string]string, error) {
|
||||
credsLabelC := C.CString(credentials.CredsLabel)
|
||||
defer C.free(unsafe.Pointer(credsLabelC))
|
||||
|
||||
var pathsC **C.char
|
||||
defer C.free(unsafe.Pointer(pathsC))
|
||||
var acctsC **C.char
|
||||
defer C.free(unsafe.Pointer(acctsC))
|
||||
var listLenC C.uint
|
||||
errMsg := C.keychain_list(credsLabelC, &pathsC, &acctsC, &listLenC)
|
||||
defer C.freeListData(&pathsC, listLenC)
|
||||
defer C.freeListData(&acctsC, listLenC)
|
||||
if errMsg != nil {
|
||||
defer C.free(unsafe.Pointer(errMsg))
|
||||
goMsg := C.GoString(errMsg)
|
||||
if goMsg == errCredentialsNotFound {
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
if goMsg == errInteractionNotAllowed {
|
||||
return nil, ErrInteractionNotAllowed
|
||||
}
|
||||
|
||||
return nil, errors.New(goMsg)
|
||||
}
|
||||
|
||||
var listLen int
|
||||
listLen = int(listLenC)
|
||||
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||
// taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
|
||||
resp := make(map[string]string)
|
||||
for i := 0; i < listLen; i++ {
|
||||
if C.GoString(pathTmp[i]) == "0" {
|
||||
continue
|
||||
}
|
||||
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func splitServer(serverURL string) (*C.struct_Server, error) {
|
||||
u, err := registryurl.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proto := C.kSecProtocolTypeHTTPS
|
||||
if u.Scheme == "http" {
|
||||
proto = C.kSecProtocolTypeHTTP
|
||||
}
|
||||
var port int
|
||||
p := u.Port()
|
||||
if p != "" {
|
||||
port, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &C.struct_Server{
|
||||
proto: C.SecProtocolType(proto),
|
||||
host: C.CString(u.Hostname()),
|
||||
port: C.uint(port),
|
||||
path: C.CString(u.Path),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func freeServer(s *C.struct_Server) {
|
||||
C.free(unsafe.Pointer(s.host))
|
||||
C.free(unsafe.Pointer(s.path))
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
#include <Security/Security.h>
|
||||
|
||||
struct Server {
|
||||
SecProtocolType proto;
|
||||
char *host;
|
||||
char *path;
|
||||
unsigned int port;
|
||||
};
|
||||
|
||||
char *keychain_add(struct Server *server, char *label, char *username, char *secret);
|
||||
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret);
|
||||
char *keychain_delete(struct Server *server);
|
||||
char *keychain_list(char *credsLabel, char *** data, char *** accts, unsigned int *list_l);
|
||||
void freeListData(char *** data, unsigned int length);
|
||||
@@ -1,213 +0,0 @@
|
||||
package osxkeychain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func TestOSXKeychainHelper(t *testing.T) {
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||
Username: "foobar",
|
||||
Secret: "foobarbaz",
|
||||
}
|
||||
creds1 := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||
Username: "foobarbaz",
|
||||
Secret: "foobar",
|
||||
}
|
||||
helper := Osxkeychain{}
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
username, secret, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if username != "foobar" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||
}
|
||||
|
||||
if secret != "foobarbaz" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||
}
|
||||
|
||||
auths, err := helper.List()
|
||||
if err != nil || len(auths) == 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
helper.Add(creds1)
|
||||
defer helper.Delete(creds1.ServerURL)
|
||||
newauths, err := helper.List()
|
||||
if len(newauths)-len(auths) != 1 {
|
||||
if err == nil {
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||
}
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||
}
|
||||
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
|
||||
// through variations on the URL
|
||||
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
// stored with port, retrieved without
|
||||
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
|
||||
|
||||
// stored as https, retrieved without scheme
|
||||
{"https://foobar.docker.io:2376", "foobar.docker.io"},
|
||||
|
||||
// stored with path, retrieved without
|
||||
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234"},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
|
||||
continue
|
||||
}
|
||||
if _, _, err := helper.Get(te.readURL); err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
|
||||
}
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
|
||||
// returned.
|
||||
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
|
||||
tests := []struct {
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
// stored as https, retrieved using http
|
||||
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
|
||||
|
||||
// stored as http, retrieved using https
|
||||
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
|
||||
|
||||
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
|
||||
{"http://foobar.docker.io", "foobar.docker.io:5678"},
|
||||
|
||||
// non-matching ports
|
||||
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
|
||||
|
||||
// non-matching ports TODO is this desired behavior? The other way round does work
|
||||
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
|
||||
|
||||
// non-matching paths
|
||||
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
|
||||
continue
|
||||
}
|
||||
if _, _, err := helper.Get(te.readURL); err == nil {
|
||||
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
|
||||
}
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
|
||||
// the keychain can be read back using the URL that was used to store them.
|
||||
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
|
||||
tests := []struct {
|
||||
url string
|
||||
}{
|
||||
{url: "foobar.docker.io"},
|
||||
{url: "foobar.docker.io:2376"},
|
||||
{url: "//foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376"},
|
||||
{url: "http://foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376/some/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.url)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.url)
|
||||
}
|
||||
|
||||
// Note that we don't delete between individual tests here, to verify that
|
||||
// subsequent stores/overwrites don't affect storing / retrieving secrets.
|
||||
for i, te := range tests {
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: te.url,
|
||||
Username: fmt.Sprintf("user-%d", i),
|
||||
Secret: fmt.Sprintf("secret-%d", i),
|
||||
}
|
||||
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
|
||||
continue
|
||||
}
|
||||
user, secret, err := helper.Get(te.url)
|
||||
if err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
|
||||
continue
|
||||
}
|
||||
if user != c.Username {
|
||||
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
|
||||
}
|
||||
if secret != c.Secret {
|
||||
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
helper := Osxkeychain{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
//go:build darwin && cgo
|
||||
|
||||
package osxkeychain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func TestOSXKeychainHelper(t *testing.T) {
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.example.com:2376/v1",
|
||||
Username: "foobar",
|
||||
Secret: "foobarbaz",
|
||||
}
|
||||
helper := Osxkeychain{}
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
username, secret, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if username != "foobar" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||
}
|
||||
|
||||
if secret != "foobarbaz" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||
}
|
||||
|
||||
auths, err := helper.List()
|
||||
if err != nil || len(auths) == 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, ok := auths[creds.ServerURL]; !ok {
|
||||
t.Fatalf("server %s not found in list, got: %+v", creds.ServerURL, auths)
|
||||
}
|
||||
|
||||
// Insert another token and check if it is in the list
|
||||
creds1 := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.example.com:2376/v2",
|
||||
Username: "foobarbaz",
|
||||
Secret: "foobar",
|
||||
}
|
||||
helper.Add(creds1)
|
||||
defer helper.Delete(creds1.ServerURL)
|
||||
|
||||
auths, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatalf("operation List failed: %+v", err)
|
||||
}
|
||||
|
||||
if _, ok := auths[creds.ServerURL]; !ok {
|
||||
t.Fatalf("server %s not found in list, got: %+v", creds.ServerURL, auths)
|
||||
}
|
||||
if _, ok := auths[creds1.ServerURL]; !ok {
|
||||
t.Fatalf("server %s not found in list, got: %+v", creds1.ServerURL, auths)
|
||||
}
|
||||
|
||||
// Delete the 1st token inserted
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auths, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatalf("operation List failed: %+v", err)
|
||||
}
|
||||
|
||||
// First token should have been deleted
|
||||
if _, ok := auths[creds.ServerURL]; ok {
|
||||
t.Fatalf("server %s was not deleted, got: %+v", creds.ServerURL, auths)
|
||||
}
|
||||
// Second token should still be there
|
||||
if _, ok := auths[creds1.ServerURL]; !ok {
|
||||
t.Fatalf("server %s not found in list, got: %+v", creds1.ServerURL, auths)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
|
||||
// through variations on the URL
|
||||
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
{
|
||||
doc: "stored with port, retrieved without",
|
||||
storeURL: "https://foobar.example.com:2376",
|
||||
readURL: "https://foobar.example.com",
|
||||
},
|
||||
{
|
||||
doc: "stored as https, retrieved without scheme",
|
||||
storeURL: "https://foobar.example.com:2376",
|
||||
readURL: "foobar.example.com",
|
||||
},
|
||||
{
|
||||
doc: "stored with path, retrieved without",
|
||||
storeURL: "https://foobar.example.com:1234/one/two",
|
||||
readURL: "https://foobar.example.com:1234",
|
||||
},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
c := &credentials.Credentials{ServerURL: tc.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL %q: %s", tc.storeURL, err)
|
||||
}
|
||||
if _, _, err := helper.Get(tc.readURL); err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q using %q: %s", tc.storeURL, tc.readURL, err)
|
||||
}
|
||||
if err := helper.Delete(tc.storeURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSXKeychainHelperStoreWithUncleanPath(t *testing.T) {
|
||||
helper := Osxkeychain{}
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://::1:8080//////location/../../hello",
|
||||
Username: "testuser",
|
||||
Secret: "testsecret",
|
||||
}
|
||||
|
||||
// Clean store before and after the test.
|
||||
defer helper.Delete(creds.ServerURL)
|
||||
if err := helper.Delete(creds.ServerURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", creds.ServerURL, err)
|
||||
}
|
||||
|
||||
// Store the credentials
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Fatalf("Error: failed to store credentials with unclean path %q: %s", creds.ServerURL, err)
|
||||
}
|
||||
|
||||
// Retrieve and verify credentials
|
||||
username, secret, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: failed to retrieve credentials with unclean path %q: %s", creds.ServerURL, err)
|
||||
}
|
||||
|
||||
if username != creds.Username {
|
||||
t.Errorf("Error: expected username %s, got %s", creds.Username, username)
|
||||
}
|
||||
if secret != creds.Secret {
|
||||
t.Errorf("Error: expected secret %s, got %s", creds.Secret, secret)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
|
||||
// returned.
|
||||
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
{
|
||||
doc: "stored as https, retrieved using http",
|
||||
storeURL: "https://foobar.example.com:2376",
|
||||
readURL: "http://foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
doc: "stored as http, retrieved using https",
|
||||
storeURL: "http://foobar.example.com:2376",
|
||||
readURL: "https://foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
// stored as http, retrieved without a scheme specified (hence, using the default https://)
|
||||
doc: "stored as http, retrieved without scheme",
|
||||
storeURL: "http://foobar.example.com",
|
||||
readURL: "foobar.example.com:5678",
|
||||
},
|
||||
{
|
||||
doc: "non-matching ports",
|
||||
storeURL: "https://foobar.example.com:1234",
|
||||
readURL: "https://foobar.example.com:5678",
|
||||
},
|
||||
// TODO: is this desired behavior? The other way round does work
|
||||
// {
|
||||
// doc: "non-matching ports (stored without port)",
|
||||
// storeURL: "https://foobar.example.com",
|
||||
// readURL: "https://foobar.example.com:5678",
|
||||
// },
|
||||
{
|
||||
doc: "non-matching paths",
|
||||
storeURL: "https://foobar.example.com:1234/one/two",
|
||||
readURL: "https://foobar.example.com:1234/five/six",
|
||||
},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
c := &credentials.Credentials{ServerURL: tc.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL %q: %s", tc.storeURL, err)
|
||||
}
|
||||
if _, _, err := helper.Get(tc.readURL); err == nil {
|
||||
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", tc.storeURL, tc.readURL)
|
||||
}
|
||||
if err := helper.Delete(tc.storeURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
|
||||
// the keychain can be read back using the URL that was used to store them.
|
||||
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
|
||||
tests := []struct {
|
||||
url string
|
||||
}{
|
||||
{url: "foobar.example.com"},
|
||||
{url: "foobar.example.com:2376"},
|
||||
{url: "//foobar.example.com:2376"},
|
||||
{url: "https://foobar.example.com:2376"},
|
||||
{url: "http://foobar.example.com:2376"},
|
||||
{url: "https://foobar.example.com:2376/some/path"},
|
||||
{url: "https://foobar.example.com:2376/some/other/path"},
|
||||
{url: "https://foobar.example.com:2376/some/other/path?foo=bar"},
|
||||
}
|
||||
|
||||
helper := Osxkeychain{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.url); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.url, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.url); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.url, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we don't delete between individual tests here, to verify that
|
||||
// subsequent stores/overwrites don't affect storing / retrieving secrets.
|
||||
for i, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.url, func(t *testing.T) {
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: tc.url,
|
||||
Username: fmt.Sprintf("user-%d", i),
|
||||
Secret: fmt.Sprintf("secret-%d", i),
|
||||
}
|
||||
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL: %s: %s", tc.url, err)
|
||||
}
|
||||
user, secret, err := helper.Get(tc.url)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: failed to read secret for URL %q: %s", tc.url, err)
|
||||
}
|
||||
if user != c.Username {
|
||||
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, tc.url)
|
||||
}
|
||||
if secret != c.Secret {
|
||||
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, tc.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
const nonExistingCred = "https://adsfasdf.invalid/asdfsdddd"
|
||||
helper := Osxkeychain{}
|
||||
_, _, err := helper.Get(nonExistingCred)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
err = helper.Delete(nonExistingCred)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
+34
-26
@@ -13,6 +13,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -28,11 +29,12 @@ type Pass struct{}
|
||||
// Ideally these would be stored as members of Pass, but since all of Pass's
|
||||
// methods have value receivers, not pointer receivers, and changing that is
|
||||
// backwards incompatible, we assume that all Pass instances share the same configuration
|
||||
|
||||
// initializationMutex is held while initializing so that only one 'pass'
|
||||
// round-tripping is done to check pass is functioning.
|
||||
var initializationMutex sync.Mutex
|
||||
var passInitialized bool
|
||||
var (
|
||||
// initializationMutex is held while initializing so that only one 'pass'
|
||||
// round-tripping is done to check pass is functioning.
|
||||
initializationMutex sync.Mutex
|
||||
passInitialized bool
|
||||
)
|
||||
|
||||
// CheckInitialized checks whether the password helper can be used. It
|
||||
// internally caches and so may be safely called multiple times with no impact
|
||||
@@ -85,8 +87,7 @@ func (p Pass) Add(creds *credentials.Credentials) error {
|
||||
return errors.New("missing credentials")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
|
||||
|
||||
encoded := encodeServerURL(creds.ServerURL)
|
||||
_, err := p.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
|
||||
return err
|
||||
}
|
||||
@@ -97,17 +98,17 @@ func (p Pass) Delete(serverURL string) error {
|
||||
return errors.New("missing server url")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||
encoded := encodeServerURL(serverURL)
|
||||
_, err := p.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
|
||||
return err
|
||||
}
|
||||
|
||||
func getPassDir() string {
|
||||
passDir := "$HOME/.password-store"
|
||||
if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" {
|
||||
passDir = envDir
|
||||
if passDir := os.Getenv("PASSWORD_STORE_DIR"); passDir != "" {
|
||||
return passDir
|
||||
}
|
||||
return os.ExpandEnv(passDir)
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".password-store")
|
||||
}
|
||||
|
||||
// listPassDir lists all the contents of a directory in the password store.
|
||||
@@ -140,23 +141,14 @@ func (p Pass) Get(serverURL string) (string, string, error) {
|
||||
return "", "", errors.New("missing server url")
|
||||
}
|
||||
|
||||
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||
|
||||
if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
encoded := encodeServerURL(serverURL)
|
||||
usernames, err := listPassDir(encoded)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
if len(usernames) < 1 {
|
||||
return "", "", fmt.Errorf("no usernames for %s", serverURL)
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||
@@ -178,7 +170,7 @@ func (p Pass) List() (map[string]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
serverURL, err := base64.URLEncoding.DecodeString(server.Name())
|
||||
serverURL, err := decodeServerURL(server.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -189,11 +181,27 @@ func (p Pass) List() (map[string]string, error) {
|
||||
}
|
||||
|
||||
if len(usernames) < 1 {
|
||||
return nil, fmt.Errorf("no usernames for %s", serverURL)
|
||||
continue
|
||||
}
|
||||
|
||||
resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||
resp[serverURL] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// encodeServerURL returns the serverURL in base64-URL encoding to use
|
||||
// as directory-name in pass storage.
|
||||
func encodeServerURL(serverURL string) string {
|
||||
return base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||
}
|
||||
|
||||
// decodeServerURL decodes base64-URL encoded serverURL. ServerURLs are
|
||||
// used in encoded format for directory-names in pass storage.
|
||||
func decodeServerURL(encodedServerURL string) (string, error) {
|
||||
serverURL, err := base64.URLEncoding.DecodeString(encodedServerURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(serverURL), nil
|
||||
}
|
||||
|
||||
+147
-34
@@ -1,6 +1,10 @@
|
||||
//go:build !windows
|
||||
|
||||
package pass
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -8,75 +12,184 @@ import (
|
||||
)
|
||||
|
||||
func TestPassHelper(t *testing.T) {
|
||||
helper := Pass{}
|
||||
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||
ServerURL: "https://foobar.example.com:2376/v1",
|
||||
Username: "nothing",
|
||||
Secret: "isthebestmeshuggahalbum",
|
||||
}
|
||||
|
||||
_ = helper.CheckInitialized()
|
||||
helper := Pass{}
|
||||
if err := helper.checkInitialized(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
helper.Add(creds)
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
creds.ServerURL = "https://foobar.docker.io:9999/v2"
|
||||
helper.Add(creds)
|
||||
u, s, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if u != creds.Username {
|
||||
t.Errorf("invalid username %s", u)
|
||||
}
|
||||
if s != creds.Secret {
|
||||
t.Errorf("invalid secret: %s", s)
|
||||
}
|
||||
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, _, err := helper.Get(creds.ServerURL); !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected credentials not found, actual: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHelperCheckInit(t *testing.T) {
|
||||
helper := Pass{}
|
||||
if v := helper.CheckInitialized(); !v {
|
||||
t.Errorf("expected true, actual: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassHelperList(t *testing.T) {
|
||||
creds := []*credentials.Credentials{
|
||||
{
|
||||
ServerURL: "https://foobar.example.com:2376/v1",
|
||||
Username: "foo",
|
||||
Secret: "isthebestmeshuggahalbum",
|
||||
},
|
||||
{
|
||||
ServerURL: "https://foobar.example.com:2375/v1",
|
||||
Username: "bar",
|
||||
Secret: "isthebestmeshuggahalbum",
|
||||
},
|
||||
}
|
||||
|
||||
helper := Pass{}
|
||||
if err := helper.checkInitialized(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for _, cred := range creds {
|
||||
if err := helper.Add(cred); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
credsList, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
for server, username := range credsList {
|
||||
if !(strings.Contains(server, "2376") ||
|
||||
strings.Contains(server, "9999")) {
|
||||
t.Fatalf("invalid url: %s", creds.ServerURL)
|
||||
if !strings.HasSuffix(server, "2376/v1") && !strings.HasSuffix(server, "2375/v1") {
|
||||
t.Errorf("invalid url: %s", server)
|
||||
}
|
||||
|
||||
if username != "nothing" {
|
||||
t.Fatalf("invalid username: %v", username)
|
||||
if username != "foo" && username != "bar" {
|
||||
t.Errorf("invalid username: %v", username)
|
||||
}
|
||||
|
||||
u, s, err := helper.Get(server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if u != username {
|
||||
t.Fatalf("invalid username %s", u)
|
||||
t.Errorf("invalid username %s", u)
|
||||
}
|
||||
|
||||
if s != "isthebestmeshuggahalbum" {
|
||||
t.Fatalf("invalid secret: %s", s)
|
||||
t.Errorf("invalid secret: %s", s)
|
||||
}
|
||||
|
||||
err = helper.Delete(server)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if err := helper.Delete(server); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, _, err = helper.Get(server)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected credentials not found, actual: %v", err)
|
||||
if _, _, err := helper.Get(server); !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected credentials not found, actual: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
credsList, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
}
|
||||
if len(credsList) != 0 {
|
||||
t.Error("didn't delete all creds?")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPassHelperWithEmptyServer verifies that empty directories (servers
|
||||
// without credentials) are ignored, but still returns credentials for other
|
||||
// servers.
|
||||
func TestPassHelperWithEmptyServer(t *testing.T) {
|
||||
helper := Pass{}
|
||||
if err := helper.checkInitialized(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(credsList) != 0 {
|
||||
t.Fatal("didn't delete all creds?")
|
||||
creds := []*credentials.Credentials{
|
||||
{
|
||||
ServerURL: "https://myreqistry.example.com:2375/v1",
|
||||
Username: "foo",
|
||||
Secret: "isthebestmeshuggahalbum",
|
||||
},
|
||||
{
|
||||
ServerURL: "https://index.example.com/v1//access-token",
|
||||
},
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for _, cred := range creds {
|
||||
_ = helper.Delete(cred.ServerURL)
|
||||
}
|
||||
})
|
||||
|
||||
for _, cred := range creds {
|
||||
if cred.Username != "" {
|
||||
if err := helper.Add(cred); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
} else {
|
||||
// No credentials; create an empty directory for this server.
|
||||
serverURL := encodeServerURL(cred.ServerURL)
|
||||
p := path.Join(getPassDir(), PASS_FOLDER, serverURL)
|
||||
if err := os.Mkdir(p, 0o755); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
credsList, err := helper.List()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(credsList) == 0 {
|
||||
t.Error("expected credentials to be returned, but got none")
|
||||
}
|
||||
for _, cred := range creds {
|
||||
if cred.Username != "" {
|
||||
userName, secret, err := helper.Get(cred.ServerURL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if userName != cred.Username {
|
||||
t.Errorf("expected username %q, actual: %q", cred.Username, userName)
|
||||
}
|
||||
if secret != cred.Secret {
|
||||
t.Errorf("expected secret %q, actual: %q", cred.Secret, secret)
|
||||
}
|
||||
} else {
|
||||
_, _, err := helper.Get(cred.ServerURL)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected credentials not found, actual: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCred(t *testing.T) {
|
||||
helper := Pass{}
|
||||
|
||||
_, _, err := helper.Get("garbage")
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected credentials not found, actual: %v", err)
|
||||
if _, _, err := helper.Get("garbage"); !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("expected credentials not found, actual: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
+53
-26
@@ -13,34 +13,61 @@ func TestHelperParseURL(t *testing.T) {
|
||||
expectedURL string
|
||||
err error
|
||||
}{
|
||||
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
|
||||
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
|
||||
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
|
||||
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
|
||||
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
|
||||
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
|
||||
{
|
||||
url: "foobar.example.com",
|
||||
expectedURL: "//foobar.example.com",
|
||||
},
|
||||
{
|
||||
url: "foobar.example.com:2376",
|
||||
expectedURL: "//foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
url: "//foobar.example.com:2376",
|
||||
expectedURL: "//foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
url: "http://foobar.example.com:2376",
|
||||
expectedURL: "http://foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
url: "https://foobar.example.com:2376",
|
||||
expectedURL: "https://foobar.example.com:2376",
|
||||
},
|
||||
{
|
||||
url: "https://foobar.example.com:2376/some/path",
|
||||
expectedURL: "https://foobar.example.com:2376/some/path",
|
||||
},
|
||||
{
|
||||
url: "https://foobar.example.com:2376/some/other/path?foo=bar",
|
||||
expectedURL: "https://foobar.example.com:2376/some/other/path",
|
||||
},
|
||||
{
|
||||
url: "/foobar.example.com",
|
||||
err: errors.New("no hostname in URL"),
|
||||
},
|
||||
{
|
||||
url: "ftp://foobar.example.com:2376",
|
||||
err: errors.New("unsupported scheme: ftp"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
u, err := Parse(te.url)
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.url, func(t *testing.T) {
|
||||
u, err := Parse(tc.url)
|
||||
|
||||
if te.err == nil && err != nil {
|
||||
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
|
||||
continue
|
||||
}
|
||||
if te.err != nil && err == nil {
|
||||
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
|
||||
continue
|
||||
}
|
||||
if te.err != nil && err.Error() != te.err.Error() {
|
||||
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
|
||||
continue
|
||||
}
|
||||
if u != nil && u.String() != te.expectedURL {
|
||||
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
|
||||
}
|
||||
if tc.err == nil && err != nil {
|
||||
t.Fatalf("Error: failed to parse URL %q: %s", tc.url, err)
|
||||
}
|
||||
if tc.err != nil && err == nil {
|
||||
t.Fatalf("Error: expected error %q, got none when parsing URL %q", tc.err, tc.url)
|
||||
}
|
||||
if tc.err != nil && err.Error() != tc.err.Error() {
|
||||
t.Fatalf("Error: expected error %q, got %q when parsing URL %q", tc.err, err, tc.url)
|
||||
}
|
||||
if u != nil && u.String() != tc.expectedURL {
|
||||
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", tc.expectedURL, u.String(), tc.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && cgo
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -1,6 +1,6 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "secretservice_linux.h"
|
||||
#include "secretservice.h"
|
||||
|
||||
const SecretSchema *docker_get_schema(void)
|
||||
{
|
||||
@@ -17,11 +17,11 @@ const SecretSchema *docker_get_schema(void)
|
||||
return &docker_schema;
|
||||
}
|
||||
|
||||
GError *add(char *label, char *server, char *username, char *secret) {
|
||||
GError *add(char *label, char *server, char *username, char *secret, char *displaylabel) {
|
||||
GError *err = NULL;
|
||||
|
||||
secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT,
|
||||
server, secret, NULL, &err,
|
||||
displaylabel, secret, NULL, &err,
|
||||
"label", label,
|
||||
"server", server,
|
||||
"username", username,
|
||||
@@ -83,6 +83,9 @@ GError *get(char *server, char **username, char **secret) {
|
||||
}
|
||||
g_free(value);
|
||||
secretValue = secret_item_get_secret(l->data);
|
||||
if (secretValue == NULL) {
|
||||
continue;
|
||||
}
|
||||
if (secret != NULL) {
|
||||
*secret = strdup(secret_value_get(secretValue, &length));
|
||||
secret_value_unref(secretValue);
|
||||
@@ -1,12 +1,15 @@
|
||||
//go:build (linux || openbsd) && cgo
|
||||
|
||||
package secretservice
|
||||
|
||||
/*
|
||||
#cgo pkg-config: libsecret-1
|
||||
|
||||
#include "secretservice_linux.h"
|
||||
#include "secretservice.h"
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
@@ -30,8 +33,10 @@ func (h Secretservice) Add(creds *credentials.Credentials) error {
|
||||
defer C.free(unsafe.Pointer(username))
|
||||
secret := C.CString(creds.Secret)
|
||||
defer C.free(unsafe.Pointer(secret))
|
||||
displayLabel := C.CString("Registry credentials for " + creds.ServerURL)
|
||||
defer C.free(unsafe.Pointer(displayLabel))
|
||||
|
||||
if err := C.add(credsLabel, server, username, secret); err != nil {
|
||||
if err := C.add(credsLabel, server, username, secret, displayLabel); err != nil {
|
||||
defer C.g_error_free(err)
|
||||
errMsg := (*C.char)(unsafe.Pointer(err.message))
|
||||
return errors.New(C.GoString(errMsg))
|
||||
@@ -6,7 +6,7 @@ const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
|
||||
|
||||
#define DOCKER_SCHEMA docker_get_schema()
|
||||
|
||||
GError *add(char *label, char *server, char *username, char *secret);
|
||||
GError *add(char *label, char *server, char *username, char *secret, char *displaylabel);
|
||||
GError *delete(char *server);
|
||||
GError *get(char *server, char **username, char **secret);
|
||||
GError *list(char *label, char *** paths, char *** accts, unsigned int *list_l);
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build (linux || openbsd) && cgo
|
||||
|
||||
package secretservice
|
||||
|
||||
import (
|
||||
@@ -11,7 +13,7 @@ func TestSecretServiceHelper(t *testing.T) {
|
||||
t.Skip("test requires gnome-keyring but travis CI doesn't have it")
|
||||
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||
ServerURL: "https://foobar.example.com:2376/v1",
|
||||
Username: "foobar",
|
||||
Secret: "foobarbaz",
|
||||
}
|
||||
@@ -28,7 +30,6 @@ func TestSecretServiceHelper(t *testing.T) {
|
||||
// remove them as they probably come from a previous failed test
|
||||
for k, v := range oldAuths {
|
||||
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
|
||||
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ wincred
|
||||
Go wrapper around the Windows Credential Manager API functions.
|
||||
|
||||
[](https://github.com/danieljoos/wincred/releases/latest)
|
||||
[](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest)
|
||||
[](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest)
|
||||
[](https://goreportcard.com/report/github.com/danieljoos/wincred)
|
||||
[](https://codecov.io/gh/danieljoos/wincred)
|
||||
[](https://pkg.go.dev/github.com/danieljoos/wincred)
|
||||
|
||||
+7
-13
@@ -31,13 +31,13 @@ func utf16FromString(str string) []uint16 {
|
||||
|
||||
// goBytes copies the given C byte array to a Go byte array (see `C.GoBytes`).
|
||||
// This function avoids having cgo as dependency.
|
||||
func goBytes(src uintptr, len uint32) []byte {
|
||||
if src == uintptr(0) {
|
||||
func goBytes(src *byte, len uint32) []byte {
|
||||
if src == nil || len == 0 {
|
||||
return []byte{}
|
||||
}
|
||||
rv := make([]byte, len)
|
||||
copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
|
||||
Data: src,
|
||||
Data: uintptr(unsafe.Pointer(src)),
|
||||
Len: int(len),
|
||||
Cap: int(len),
|
||||
})))
|
||||
@@ -59,7 +59,7 @@ func sysToCredential(cred *sysCREDENTIAL) (result *Credential) {
|
||||
result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize)
|
||||
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
||||
attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{
|
||||
Data: cred.Attributes,
|
||||
Data: uintptr(unsafe.Pointer(cred.Attributes)),
|
||||
Len: int(cred.AttributeCount),
|
||||
Cap: int(cred.AttributeCount),
|
||||
}))
|
||||
@@ -85,17 +85,13 @@ func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) {
|
||||
result.LastWritten = syscall.NsecToFiletime(cred.LastWritten.UnixNano())
|
||||
result.CredentialBlobSize = uint32(len(cred.CredentialBlob))
|
||||
if len(cred.CredentialBlob) > 0 {
|
||||
result.CredentialBlob = uintptr(unsafe.Pointer(&cred.CredentialBlob[0]))
|
||||
} else {
|
||||
result.CredentialBlob = 0
|
||||
result.CredentialBlob = &cred.CredentialBlob[0]
|
||||
}
|
||||
result.Persist = uint32(cred.Persist)
|
||||
result.AttributeCount = uint32(len(cred.Attributes))
|
||||
attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes))
|
||||
if len(attributes) > 0 {
|
||||
result.Attributes = uintptr(unsafe.Pointer(&attributes[0]))
|
||||
} else {
|
||||
result.Attributes = 0
|
||||
result.Attributes = &attributes[0]
|
||||
}
|
||||
for i := range cred.Attributes {
|
||||
inAttr := &cred.Attributes[i]
|
||||
@@ -104,9 +100,7 @@ func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) {
|
||||
outAttr.Flags = 0
|
||||
outAttr.ValueSize = uint32(len(inAttr.Value))
|
||||
if len(inAttr.Value) > 0 {
|
||||
outAttr.Value = uintptr(unsafe.Pointer(&inAttr.Value[0]))
|
||||
} else {
|
||||
outAttr.Value = 0
|
||||
outAttr.Value = &inAttr.Value[0]
|
||||
}
|
||||
}
|
||||
result.TargetAlias, _ = syscall.UTF16PtrFromString(cred.TargetAlias)
|
||||
|
||||
+23
-15
@@ -1,21 +1,24 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package wincred
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
syscall "golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||
procCredRead proc = modadvapi32.NewProc("CredReadW")
|
||||
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
|
||||
procCredRead = modadvapi32.NewProc("CredReadW")
|
||||
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
|
||||
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
|
||||
procCredFree proc = modadvapi32.NewProc("CredFree")
|
||||
procCredEnumerate proc = modadvapi32.NewProc("CredEnumerateW")
|
||||
procCredEnumerate = modadvapi32.NewProc("CredEnumerateW")
|
||||
)
|
||||
|
||||
// Interface for syscall.Proc: helps testing
|
||||
@@ -29,12 +32,12 @@ type sysCREDENTIAL struct {
|
||||
Type uint32
|
||||
TargetName *uint16
|
||||
Comment *uint16
|
||||
LastWritten syscall.Filetime
|
||||
LastWritten windows.Filetime
|
||||
CredentialBlobSize uint32
|
||||
CredentialBlob uintptr
|
||||
CredentialBlob *byte
|
||||
Persist uint32
|
||||
AttributeCount uint32
|
||||
Attributes uintptr
|
||||
Attributes *sysCREDENTIAL_ATTRIBUTE
|
||||
TargetAlias *uint16
|
||||
UserName *uint16
|
||||
}
|
||||
@@ -44,7 +47,7 @@ type sysCREDENTIAL_ATTRIBUTE struct {
|
||||
Keyword *uint16
|
||||
Flags uint32
|
||||
ValueSize uint32
|
||||
Value uintptr
|
||||
Value *byte
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw
|
||||
@@ -59,15 +62,17 @@ const (
|
||||
sysCRED_TYPE_DOMAIN_EXTENDED sysCRED_TYPE = 0x6
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes
|
||||
sysERROR_NOT_FOUND = syscall.Errno(1168)
|
||||
sysERROR_INVALID_PARAMETER = syscall.Errno(87)
|
||||
sysERROR_NOT_FOUND = windows.Errno(1168)
|
||||
sysERROR_INVALID_PARAMETER = windows.Errno(87)
|
||||
sysERROR_BAD_USERNAME = windows.Errno(2202)
|
||||
)
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw
|
||||
func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) {
|
||||
var pcred *sysCREDENTIAL
|
||||
targetNamePtr, _ := syscall.UTF16PtrFromString(targetName)
|
||||
ret, _, err := procCredRead.Call(
|
||||
targetNamePtr, _ := windows.UTF16PtrFromString(targetName)
|
||||
ret, _, err := syscall.SyscallN(
|
||||
procCredRead.Addr(),
|
||||
uintptr(unsafe.Pointer(targetNamePtr)),
|
||||
uintptr(typ),
|
||||
0,
|
||||
@@ -89,6 +94,8 @@ func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error {
|
||||
uintptr(unsafe.Pointer(ncred)),
|
||||
0,
|
||||
)
|
||||
// Make sure everything reachable from ncred stays alive through the call.
|
||||
runtime.KeepAlive(ncred)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
@@ -98,7 +105,7 @@ func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error {
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew
|
||||
func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error {
|
||||
targetNamePtr, _ := syscall.UTF16PtrFromString(cred.TargetName)
|
||||
targetNamePtr, _ := windows.UTF16PtrFromString(cred.TargetName)
|
||||
ret, _, err := procCredDelete.Call(
|
||||
uintptr(unsafe.Pointer(targetNamePtr)),
|
||||
uintptr(typ),
|
||||
@@ -117,9 +124,10 @@ func sysCredEnumerate(filter string, all bool) ([]*Credential, error) {
|
||||
var pcreds uintptr
|
||||
var filterPtr *uint16
|
||||
if !all {
|
||||
filterPtr, _ = syscall.UTF16PtrFromString(filter)
|
||||
filterPtr, _ = windows.UTF16PtrFromString(filter)
|
||||
}
|
||||
ret, _, err := procCredEnumerate.Call(
|
||||
ret, _, err := syscall.SyscallN(
|
||||
procCredEnumerate.Addr(),
|
||||
uintptr(unsafe.Pointer(filterPtr)),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&count)),
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package wincred
|
||||
@@ -17,6 +18,7 @@ const (
|
||||
|
||||
sysERROR_NOT_FOUND = syscall.Errno(1)
|
||||
sysERROR_INVALID_PARAMETER = syscall.Errno(1)
|
||||
sysERROR_BAD_USERNAME = syscall.Errno(1)
|
||||
)
|
||||
|
||||
func sysCredRead(...interface{}) (*Credential, error) {
|
||||
|
||||
+3
@@ -16,6 +16,9 @@ const (
|
||||
// This error constant can be used to check if the given function parameters were invalid.
|
||||
// For example when trying to create a new generic credential with an empty target name.
|
||||
ErrInvalidParameter = sysERROR_INVALID_PARAMETER
|
||||
|
||||
// ErrBadUsername is returned when the credential's username is invalid.
|
||||
ErrBadUsername = sysERROR_BAD_USERNAME
|
||||
)
|
||||
|
||||
// GetGenericCredential fetches the generic credential with the given name from Windows credential manager.
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
vendor
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
linters-settings:
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- elseif
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gocritic
|
||||
- unconvert
|
||||
- revive
|
||||
- govet
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Keybase
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
# Go Keychain
|
||||
|
||||
[](https://github.com/keybase/go-keychain/actions)
|
||||
|
||||
A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang).
|
||||
|
||||
Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to
|
||||
a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice.
|
||||
|
||||
```go
|
||||
import "github.com/keybase/go-keychain"
|
||||
```
|
||||
|
||||
## Mac/iOS Usage
|
||||
|
||||
The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go.
|
||||
|
||||
#### Add Item
|
||||
|
||||
```go
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassGenericPassword)
|
||||
item.SetService("MyService")
|
||||
item.SetAccount("gabriel")
|
||||
item.SetLabel("A label")
|
||||
item.SetAccessGroup("A123456789.group.com.mycorp")
|
||||
item.SetData([]byte("toomanysecrets"))
|
||||
item.SetSynchronizable(keychain.SynchronizableNo)
|
||||
item.SetAccessible(keychain.AccessibleWhenUnlocked)
|
||||
err := keychain.AddItem(item)
|
||||
|
||||
if err == keychain.ErrorDuplicateItem {
|
||||
// Duplicate
|
||||
}
|
||||
```
|
||||
|
||||
#### Query Item
|
||||
|
||||
Query for multiple results, returning attributes:
|
||||
|
||||
```go
|
||||
query := keychain.NewItem()
|
||||
query.SetSecClass(keychain.SecClassGenericPassword)
|
||||
query.SetService(service)
|
||||
query.SetAccount(account)
|
||||
query.SetAccessGroup(accessGroup)
|
||||
query.SetMatchLimit(keychain.MatchLimitAll)
|
||||
query.SetReturnAttributes(true)
|
||||
results, err := keychain.QueryItem(query)
|
||||
if err != nil {
|
||||
// Error
|
||||
} else {
|
||||
for _, r := range results {
|
||||
fmt.Printf("%#v\n", r)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Query for a single result, returning data:
|
||||
|
||||
```go
|
||||
query := keychain.NewItem()
|
||||
query.SetSecClass(keychain.SecClassGenericPassword)
|
||||
query.SetService(service)
|
||||
query.SetAccount(account)
|
||||
query.SetAccessGroup(accessGroup)
|
||||
query.SetMatchLimit(keychain.MatchLimitOne)
|
||||
query.SetReturnData(true)
|
||||
results, err := keychain.QueryItem(query)
|
||||
if err != nil {
|
||||
// Error
|
||||
} else if len(results) != 1 {
|
||||
// Not found
|
||||
} else {
|
||||
password := string(results[0].Data)
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete Item
|
||||
|
||||
Delete a generic password item with service and account:
|
||||
|
||||
```go
|
||||
item := keychain.NewItem()
|
||||
item.SetSecClass(keychain.SecClassGenericPassword)
|
||||
item.SetService(service)
|
||||
item.SetAccount(account)
|
||||
err := keychain.DeleteItem(item)
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
There are some convenience methods for generic password:
|
||||
|
||||
```go
|
||||
// Create generic password item with service, account, label, password, access group
|
||||
item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp")
|
||||
item.SetSynchronizable(keychain.SynchronizableNo)
|
||||
item.SetAccessible(keychain.AccessibleWhenUnlocked)
|
||||
err := keychain.AddItem(item)
|
||||
if err == keychain.ErrorDuplicateItem {
|
||||
// Duplicate
|
||||
}
|
||||
|
||||
password, err := keychain.GetGenericPassword("MyService", "gabriel", "A label", "A123456789.group.com.mycorp")
|
||||
|
||||
accounts, err := keychain.GetGenericPasswordAccounts("MyService")
|
||||
// Should have 1 account == "gabriel"
|
||||
|
||||
err := keychain.DeleteGenericPasswordItem("MyService", "gabriel")
|
||||
if err == keychain.ErrorItemNotFound {
|
||||
// Not found
|
||||
}
|
||||
```
|
||||
|
||||
## iOS
|
||||
|
||||
Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS.
|
||||
|
||||
To re-generate framework:
|
||||
|
||||
```
|
||||
(cd bind && gomobile bind -target=ios -tags=ios -o ../ios/bind.framework)
|
||||
```
|
||||
|
||||
Post issues to: https://github.com/keybase/keybase-issues
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
//go:build darwin || ios
|
||||
// +build darwin ios
|
||||
|
||||
package keychain
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework CoreFoundation
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
// Can't cast a *uintptr to *unsafe.Pointer in Go, and casting
|
||||
// C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to
|
||||
// do the casting in C (where it's safe).
|
||||
|
||||
// We add a suffix to the C functions below, because we copied this
|
||||
// file from go-kext, which means that any project that depends on this
|
||||
// package and go-kext would run into duplicate symbol errors otherwise.
|
||||
//
|
||||
// TODO: Move this file into its own package depended on by go-kext
|
||||
// and this package.
|
||||
|
||||
CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) {
|
||||
return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks);
|
||||
}
|
||||
|
||||
CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) {
|
||||
return CFArrayCreate(allocator, (const void **)values, numValues, callBacks);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Release releases memory pointed to by a CFTypeRef.
|
||||
func Release(ref C.CFTypeRef) {
|
||||
C.CFRelease(ref)
|
||||
}
|
||||
|
||||
// BytesToCFData will return a CFDataRef and if non-nil, must be released with
|
||||
// Release(ref).
|
||||
func BytesToCFData(b []byte) (C.CFDataRef, error) {
|
||||
if uint64(len(b)) > math.MaxUint32 {
|
||||
return 0, errors.New("Data is too large")
|
||||
}
|
||||
var p *C.UInt8
|
||||
if len(b) > 0 {
|
||||
p = (*C.UInt8)(&b[0])
|
||||
}
|
||||
cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b)))
|
||||
if cfData == 0 {
|
||||
return 0, fmt.Errorf("CFDataCreate failed")
|
||||
}
|
||||
return cfData, nil
|
||||
}
|
||||
|
||||
// CFDataToBytes converts CFData to bytes.
|
||||
func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) {
|
||||
return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil
|
||||
}
|
||||
|
||||
// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be
|
||||
// released with Release(ref).
|
||||
func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) {
|
||||
var keys, values []C.uintptr_t
|
||||
for key, value := range m {
|
||||
keys = append(keys, C.uintptr_t(key))
|
||||
values = append(values, C.uintptr_t(value))
|
||||
}
|
||||
numValues := len(values)
|
||||
var keysPointer, valuesPointer *C.uintptr_t
|
||||
if numValues > 0 {
|
||||
keysPointer = &keys[0]
|
||||
valuesPointer = &values[0]
|
||||
}
|
||||
cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues),
|
||||
&C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint
|
||||
if cfDict == 0 {
|
||||
return 0, fmt.Errorf("CFDictionaryCreate failed")
|
||||
}
|
||||
return cfDict, nil
|
||||
}
|
||||
|
||||
// CFDictionaryToMap converts CFDictionaryRef to a map.
|
||||
func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) {
|
||||
count := C.CFDictionaryGetCount(cfDict)
|
||||
if count > 0 {
|
||||
keys := make([]C.CFTypeRef, count)
|
||||
values := make([]C.CFTypeRef, count)
|
||||
C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0])))
|
||||
m = make(map[C.CFTypeRef]C.CFTypeRef, count)
|
||||
for i := C.CFIndex(0); i < count; i++ {
|
||||
m[keys[i]] = values[i]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Int32ToCFNumber will return a CFNumberRef, must be released with Release(ref).
|
||||
func Int32ToCFNumber(u int32) C.CFNumberRef {
|
||||
sint := C.SInt32(u)
|
||||
p := unsafe.Pointer(&sint)
|
||||
return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p)
|
||||
}
|
||||
|
||||
// StringToCFString will return a CFStringRef and if non-nil, must be released with
|
||||
// Release(ref).
|
||||
func StringToCFString(s string) (C.CFStringRef, error) {
|
||||
if !utf8.ValidString(s) {
|
||||
return 0, errors.New("Invalid UTF-8 string")
|
||||
}
|
||||
if uint64(len(s)) > math.MaxUint32 {
|
||||
return 0, errors.New("String is too large")
|
||||
}
|
||||
|
||||
bytes := []byte(s)
|
||||
var p *C.UInt8
|
||||
if len(bytes) > 0 {
|
||||
p = (*C.UInt8)(&bytes[0])
|
||||
}
|
||||
return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil
|
||||
}
|
||||
|
||||
// CFStringToString converts a CFStringRef to a string.
|
||||
func CFStringToString(s C.CFStringRef) string {
|
||||
p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8)
|
||||
if p != nil {
|
||||
return C.GoString(p)
|
||||
}
|
||||
length := C.CFStringGetLength(s)
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8)
|
||||
if maxBufLen == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]byte, maxBufLen)
|
||||
var usedBufLen C.CFIndex
|
||||
_ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen)
|
||||
return string(buf[:usedBufLen])
|
||||
}
|
||||
|
||||
// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with
|
||||
// Release(ref).
|
||||
func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef {
|
||||
var values []C.uintptr_t
|
||||
for _, value := range a {
|
||||
values = append(values, C.uintptr_t(value))
|
||||
}
|
||||
numValues := len(values)
|
||||
var valuesPointer *C.uintptr_t
|
||||
if numValues > 0 {
|
||||
valuesPointer = &values[0]
|
||||
}
|
||||
return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint
|
||||
}
|
||||
|
||||
// CFArrayToArray converts a CFArrayRef to an array of CFTypes.
|
||||
func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) {
|
||||
count := C.CFArrayGetCount(cfArray)
|
||||
if count > 0 {
|
||||
a = make([]C.CFTypeRef, count)
|
||||
C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0])))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Convertable knows how to convert an instance to a CFTypeRef.
|
||||
type Convertable interface {
|
||||
Convert() (C.CFTypeRef, error)
|
||||
}
|
||||
|
||||
// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil,
|
||||
// must be released with Release(ref).
|
||||
func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) {
|
||||
m := make(map[C.CFTypeRef]C.CFTypeRef)
|
||||
for key, i := range attr {
|
||||
var valueRef C.CFTypeRef
|
||||
switch val := i.(type) {
|
||||
default:
|
||||
return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i))
|
||||
case C.CFTypeRef:
|
||||
valueRef = val
|
||||
case bool:
|
||||
if val {
|
||||
valueRef = C.CFTypeRef(C.kCFBooleanTrue)
|
||||
} else {
|
||||
valueRef = C.CFTypeRef(C.kCFBooleanFalse)
|
||||
}
|
||||
case int32:
|
||||
valueRef = C.CFTypeRef(Int32ToCFNumber(val))
|
||||
defer Release(valueRef)
|
||||
case []byte:
|
||||
bytesRef, err := BytesToCFData(val)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
valueRef = C.CFTypeRef(bytesRef)
|
||||
defer Release(valueRef)
|
||||
case string:
|
||||
stringRef, err := StringToCFString(val)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
valueRef = C.CFTypeRef(stringRef)
|
||||
defer Release(valueRef)
|
||||
case Convertable:
|
||||
convertedRef, err := val.Convert()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
valueRef = convertedRef
|
||||
defer Release(valueRef)
|
||||
}
|
||||
keyRef, err := StringToCFString(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m[C.CFTypeRef(keyRef)] = valueRef
|
||||
defer Release(C.CFTypeRef(keyRef))
|
||||
}
|
||||
|
||||
cfDict, err := MapToCFDictionary(m)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return cfDict, nil
|
||||
}
|
||||
|
||||
// CFTypeDescription returns type string for CFTypeRef.
|
||||
func CFTypeDescription(ref C.CFTypeRef) string {
|
||||
typeID := C.CFGetTypeID(ref)
|
||||
typeDesc := C.CFCopyTypeIDDescription(typeID)
|
||||
defer Release(C.CFTypeRef(typeDesc))
|
||||
return CFStringToString(typeDesc)
|
||||
}
|
||||
|
||||
// Convert converts a CFTypeRef to a go instance.
|
||||
func Convert(ref C.CFTypeRef) (interface{}, error) {
|
||||
typeID := C.CFGetTypeID(ref)
|
||||
if typeID == C.CFStringGetTypeID() {
|
||||
return CFStringToString(C.CFStringRef(ref)), nil
|
||||
} else if typeID == C.CFDictionaryGetTypeID() {
|
||||
return ConvertCFDictionary(C.CFDictionaryRef(ref))
|
||||
} else if typeID == C.CFArrayGetTypeID() {
|
||||
arr := CFArrayToArray(C.CFArrayRef(ref))
|
||||
results := make([]interface{}, 0, len(arr))
|
||||
for _, ref := range arr {
|
||||
v, err := Convert(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, v)
|
||||
}
|
||||
return results, nil
|
||||
} else if typeID == C.CFDataGetTypeID() {
|
||||
b, err := CFDataToBytes(C.CFDataRef(ref))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
} else if typeID == C.CFNumberGetTypeID() {
|
||||
return CFNumberToInterface(C.CFNumberRef(ref)), nil
|
||||
} else if typeID == C.CFBooleanGetTypeID() {
|
||||
if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref))
|
||||
}
|
||||
|
||||
// ConvertCFDictionary converts a CFDictionary to map (deep).
|
||||
func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) {
|
||||
m := CFDictionaryToMap(d)
|
||||
result := make(map[interface{}]interface{})
|
||||
|
||||
for k, v := range m {
|
||||
gk, err := Convert(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gv, err := Convert(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[gk] = gv
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric
|
||||
// type.
|
||||
// This code is from github.com/kballard/go-osx-plist.
|
||||
func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} {
|
||||
typ := C.CFNumberGetType(cfNumber)
|
||||
switch typ {
|
||||
case C.kCFNumberSInt8Type:
|
||||
var sint C.SInt8
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint
|
||||
return int8(sint)
|
||||
case C.kCFNumberSInt16Type:
|
||||
var sint C.SInt16
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint
|
||||
return int16(sint)
|
||||
case C.kCFNumberSInt32Type:
|
||||
var sint C.SInt32
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint
|
||||
return int32(sint)
|
||||
case C.kCFNumberSInt64Type:
|
||||
var sint C.SInt64
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint
|
||||
return int64(sint)
|
||||
case C.kCFNumberFloat32Type:
|
||||
var float C.Float32
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint
|
||||
return float32(float)
|
||||
case C.kCFNumberFloat64Type:
|
||||
var float C.Float64
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint
|
||||
return float64(float)
|
||||
case C.kCFNumberCharType:
|
||||
var char C.char
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) //nolint
|
||||
return byte(char)
|
||||
case C.kCFNumberShortType:
|
||||
var short C.short
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) //nolint
|
||||
return int16(short)
|
||||
case C.kCFNumberIntType:
|
||||
var i C.int
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) //nolint
|
||||
return int32(i)
|
||||
case C.kCFNumberLongType:
|
||||
var long C.long
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) //nolint
|
||||
return int(long)
|
||||
case C.kCFNumberLongLongType:
|
||||
// This is the only type that may actually overflow us
|
||||
var longlong C.longlong
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) //nolint
|
||||
return int64(longlong)
|
||||
case C.kCFNumberFloatType:
|
||||
var float C.float
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint
|
||||
return float32(float)
|
||||
case C.kCFNumberDoubleType:
|
||||
var double C.double
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) //nolint
|
||||
return float64(double)
|
||||
case C.kCFNumberCFIndexType:
|
||||
// CFIndex is a long
|
||||
var index C.CFIndex
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) //nolint
|
||||
return int(index)
|
||||
case C.kCFNumberNSIntegerType:
|
||||
// We don't have a definition of NSInteger, but we know it's either an int or a long
|
||||
var nsInt C.long
|
||||
C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) //nolint
|
||||
return int(nsInt)
|
||||
}
|
||||
panic("Unknown CFNumber type")
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
//go:build darwin || ios
|
||||
// +build darwin ios
|
||||
|
||||
package keychain
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework CoreFoundation
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
const nsPerSec = 1000 * 1000 * 1000
|
||||
|
||||
// absoluteTimeIntervalSince1970() returns the number of seconds from
|
||||
// the Unix epoch (1970-01-01T00:00:00+00:00) to the Core Foundation
|
||||
// absolute reference date (2001-01-01T00:00:00+00:00). It should be
|
||||
// exactly 978307200.
|
||||
func absoluteTimeIntervalSince1970() int64 {
|
||||
return int64(C.kCFAbsoluteTimeIntervalSince1970)
|
||||
}
|
||||
|
||||
func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime {
|
||||
// Subtract as int64s first before converting to floating
|
||||
// point to minimize precision loss (assuming the given time
|
||||
// isn't much earlier than the Core Foundation absolute
|
||||
// reference date).
|
||||
abs := s - absoluteTimeIntervalSince1970()
|
||||
return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec
|
||||
}
|
||||
|
||||
func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) {
|
||||
i, frac := math.Modf(float64(abs))
|
||||
return int64(i) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec)
|
||||
}
|
||||
|
||||
// TimeToCFDate will convert the given time.Time to a CFDateRef, which
|
||||
// must be released with Release(ref).
|
||||
func TimeToCFDate(t time.Time) C.CFDateRef {
|
||||
s := t.Unix()
|
||||
ns := int64(t.Nanosecond())
|
||||
abs := unixToAbsoluteTime(s, ns)
|
||||
return C.CFDateCreate(C.kCFAllocatorDefault, abs)
|
||||
}
|
||||
|
||||
// CFDateToTime will convert the given CFDateRef to a time.Time.
|
||||
func CFDateToTime(d C.CFDateRef) time.Time {
|
||||
abs := C.CFDateGetAbsoluteTime(d)
|
||||
s, ns := absoluteTimeToUnix(abs)
|
||||
return time.Unix(s, ns)
|
||||
}
|
||||
|
||||
// Wrappers around C functions for testing.
|
||||
|
||||
func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime {
|
||||
return C.CFDateGetAbsoluteTime(d)
|
||||
}
|
||||
|
||||
func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef {
|
||||
return C.CFDateCreate(C.kCFAllocatorDefault, abs)
|
||||
}
|
||||
|
||||
func releaseCFDate(d C.CFDateRef) {
|
||||
Release(C.CFTypeRef(d))
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//go:build darwin && ios
|
||||
// +build darwin,ios
|
||||
|
||||
package keychain
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework CoreFoundation -framework Security
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible))
|
||||
var accessibleTypeRef = map[Accessible]C.CFTypeRef{
|
||||
AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked),
|
||||
AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock),
|
||||
AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways),
|
||||
AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
|
||||
AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
|
||||
AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),
|
||||
AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly),
|
||||
}
|
||||
+653
@@ -0,0 +1,653 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package keychain
|
||||
|
||||
// See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below.
|
||||
|
||||
// Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html .
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework CoreFoundation -framework Security
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Error defines keychain errors
|
||||
type Error int
|
||||
|
||||
var (
|
||||
// ErrorUnimplemented corresponds to errSecUnimplemented result code
|
||||
ErrorUnimplemented = Error(C.errSecUnimplemented)
|
||||
// ErrorParam corresponds to errSecParam result code
|
||||
ErrorParam = Error(C.errSecParam)
|
||||
// ErrorAllocate corresponds to errSecAllocate result code
|
||||
ErrorAllocate = Error(C.errSecAllocate)
|
||||
// ErrorNotAvailable corresponds to errSecNotAvailable result code
|
||||
ErrorNotAvailable = Error(C.errSecNotAvailable)
|
||||
// ErrorAuthFailed corresponds to errSecAuthFailed result code
|
||||
ErrorAuthFailed = Error(C.errSecAuthFailed)
|
||||
// ErrorDuplicateItem corresponds to errSecDuplicateItem result code
|
||||
ErrorDuplicateItem = Error(C.errSecDuplicateItem)
|
||||
// ErrorItemNotFound corresponds to errSecItemNotFound result code
|
||||
ErrorItemNotFound = Error(C.errSecItemNotFound)
|
||||
// ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code
|
||||
ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed)
|
||||
// ErrorDecode corresponds to errSecDecode result code
|
||||
ErrorDecode = Error(C.errSecDecode)
|
||||
// ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code
|
||||
ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain)
|
||||
// ErrorNoAccessForItem corresponds to errSecNoAccessForItem result code
|
||||
ErrorNoAccessForItem = Error(C.errSecNoAccessForItem)
|
||||
// ErrorReadOnly corresponds to errSecReadOnly result code
|
||||
ErrorReadOnly = Error(C.errSecReadOnly)
|
||||
// ErrorInvalidKeychain corresponds to errSecInvalidKeychain result code
|
||||
ErrorInvalidKeychain = Error(C.errSecInvalidKeychain)
|
||||
// ErrorDuplicateKeyChain corresponds to errSecDuplicateKeychain result code
|
||||
ErrorDuplicateKeyChain = Error(C.errSecDuplicateKeychain)
|
||||
// ErrorWrongVersion corresponds to errSecWrongSecVersion result code
|
||||
ErrorWrongVersion = Error(C.errSecWrongSecVersion)
|
||||
// ErrorReadonlyAttribute corresponds to errSecReadOnlyAttr result code
|
||||
ErrorReadonlyAttribute = Error(C.errSecReadOnlyAttr)
|
||||
// ErrorInvalidSearchRef corresponds to errSecInvalidSearchRef result code
|
||||
ErrorInvalidSearchRef = Error(C.errSecInvalidSearchRef)
|
||||
// ErrorInvalidItemRef corresponds to errSecInvalidItemRef result code
|
||||
ErrorInvalidItemRef = Error(C.errSecInvalidItemRef)
|
||||
// ErrorDataNotAvailable corresponds to errSecDataNotAvailable result code
|
||||
ErrorDataNotAvailable = Error(C.errSecDataNotAvailable)
|
||||
// ErrorDataNotModifiable corresponds to errSecDataNotModifiable result code
|
||||
ErrorDataNotModifiable = Error(C.errSecDataNotModifiable)
|
||||
// ErrorInvalidOwnerEdit corresponds to errSecInvalidOwnerEdit result code
|
||||
ErrorInvalidOwnerEdit = Error(C.errSecInvalidOwnerEdit)
|
||||
// ErrorUserCanceled corresponds to errSecUserCanceled result code
|
||||
ErrorUserCanceled = Error(C.errSecUserCanceled)
|
||||
)
|
||||
|
||||
func checkError(errCode C.OSStatus) error {
|
||||
if errCode == C.errSecSuccess {
|
||||
return nil
|
||||
}
|
||||
return Error(errCode)
|
||||
}
|
||||
|
||||
func (k Error) Error() (msg string) {
|
||||
// SecCopyErrorMessageString is only available on OSX, so derive manually.
|
||||
// Messages derived from `$ security error $errcode`.
|
||||
switch k {
|
||||
case ErrorUnimplemented:
|
||||
msg = "Function or operation not implemented."
|
||||
case ErrorParam:
|
||||
msg = "One or more parameters passed to the function were not valid."
|
||||
case ErrorAllocate:
|
||||
msg = "Failed to allocate memory."
|
||||
case ErrorNotAvailable:
|
||||
msg = "No keychain is available. You may need to restart your computer."
|
||||
case ErrorAuthFailed:
|
||||
msg = "The user name or passphrase you entered is not correct."
|
||||
case ErrorDuplicateItem:
|
||||
msg = "The specified item already exists in the keychain."
|
||||
case ErrorItemNotFound:
|
||||
msg = "The specified item could not be found in the keychain."
|
||||
case ErrorInteractionNotAllowed:
|
||||
msg = "User interaction is not allowed."
|
||||
case ErrorDecode:
|
||||
msg = "Unable to decode the provided data."
|
||||
case ErrorNoSuchKeychain:
|
||||
msg = "The specified keychain could not be found."
|
||||
case ErrorNoAccessForItem:
|
||||
msg = "The specified item has no access control."
|
||||
case ErrorReadOnly:
|
||||
msg = "Read-only error."
|
||||
case ErrorReadonlyAttribute:
|
||||
msg = "The attribute is read-only."
|
||||
case ErrorInvalidKeychain:
|
||||
msg = "The keychain is not valid."
|
||||
case ErrorDuplicateKeyChain:
|
||||
msg = "A keychain with the same name already exists."
|
||||
case ErrorWrongVersion:
|
||||
msg = "The version is incorrect."
|
||||
case ErrorInvalidItemRef:
|
||||
msg = "The item reference is invalid."
|
||||
case ErrorInvalidSearchRef:
|
||||
msg = "The search reference is invalid."
|
||||
case ErrorDataNotAvailable:
|
||||
msg = "The data is not available."
|
||||
case ErrorDataNotModifiable:
|
||||
msg = "The data is not modifiable."
|
||||
case ErrorInvalidOwnerEdit:
|
||||
msg = "An invalid attempt to change the owner of an item."
|
||||
case ErrorUserCanceled:
|
||||
msg = "User canceled the operation."
|
||||
default:
|
||||
msg = "Keychain Error."
|
||||
}
|
||||
return fmt.Sprintf("%s (%d)", msg, k)
|
||||
}
|
||||
|
||||
// SecClass is the items class code
|
||||
type SecClass int
|
||||
|
||||
// Keychain Item Classes
|
||||
var (
|
||||
/*
|
||||
kSecClassGenericPassword item attributes:
|
||||
kSecAttrAccess (OS X only)
|
||||
kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified)
|
||||
kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified)
|
||||
kSecAttrAccount
|
||||
kSecAttrService
|
||||
*/
|
||||
SecClassGenericPassword SecClass = 1
|
||||
SecClassInternetPassword SecClass = 2
|
||||
)
|
||||
|
||||
// SecClassKey is the key type for SecClass
|
||||
var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass))
|
||||
var secClassTypeRef = map[SecClass]C.CFTypeRef{
|
||||
SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword),
|
||||
SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword),
|
||||
}
|
||||
|
||||
var (
|
||||
// ServiceKey is for kSecAttrService
|
||||
ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService))
|
||||
|
||||
// ServerKey is for kSecAttrServer
|
||||
ServerKey = attrKey(C.CFTypeRef(C.kSecAttrServer))
|
||||
// ProtocolKey is for kSecAttrProtocol
|
||||
ProtocolKey = attrKey(C.CFTypeRef(C.kSecAttrProtocol))
|
||||
// AuthenticationTypeKey is for kSecAttrAuthenticationType
|
||||
AuthenticationTypeKey = attrKey(C.CFTypeRef(C.kSecAttrAuthenticationType))
|
||||
// PortKey is for kSecAttrPort
|
||||
PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort))
|
||||
// PathKey is for kSecAttrPath
|
||||
PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath))
|
||||
|
||||
// LabelKey is for kSecAttrLabel
|
||||
LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel))
|
||||
// AccountKey is for kSecAttrAccount
|
||||
AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount))
|
||||
// AccessGroupKey is for kSecAttrAccessGroup
|
||||
AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup))
|
||||
// DataKey is for kSecValueData
|
||||
DataKey = attrKey(C.CFTypeRef(C.kSecValueData))
|
||||
// DescriptionKey is for kSecAttrDescription
|
||||
DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription))
|
||||
// CommentKey is for kSecAttrComment
|
||||
CommentKey = attrKey(C.CFTypeRef(C.kSecAttrComment))
|
||||
// CreationDateKey is for kSecAttrCreationDate
|
||||
CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate))
|
||||
// ModificationDateKey is for kSecAttrModificationDate
|
||||
ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate))
|
||||
)
|
||||
|
||||
// Synchronizable is the items synchronizable status
|
||||
type Synchronizable int
|
||||
|
||||
const (
|
||||
// SynchronizableDefault is the default setting
|
||||
SynchronizableDefault Synchronizable = 0
|
||||
// SynchronizableAny is for kSecAttrSynchronizableAny
|
||||
SynchronizableAny = 1
|
||||
// SynchronizableYes enables synchronization
|
||||
SynchronizableYes = 2
|
||||
// SynchronizableNo disables synchronization
|
||||
SynchronizableNo = 3
|
||||
)
|
||||
|
||||
// SynchronizableKey is the key type for Synchronizable
|
||||
var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable))
|
||||
var syncTypeRef = map[Synchronizable]C.CFTypeRef{
|
||||
SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny),
|
||||
SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue),
|
||||
SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse),
|
||||
}
|
||||
|
||||
// Accessible is the items accessibility
|
||||
type Accessible int
|
||||
|
||||
const (
|
||||
// AccessibleDefault is the default
|
||||
AccessibleDefault Accessible = 0
|
||||
// AccessibleWhenUnlocked is when unlocked
|
||||
AccessibleWhenUnlocked = 1
|
||||
// AccessibleAfterFirstUnlock is after first unlock
|
||||
AccessibleAfterFirstUnlock = 2
|
||||
// AccessibleAlways is always
|
||||
AccessibleAlways = 3
|
||||
// AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set
|
||||
AccessibleWhenPasscodeSetThisDeviceOnly = 4
|
||||
// AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only
|
||||
AccessibleWhenUnlockedThisDeviceOnly = 5
|
||||
// AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only
|
||||
AccessibleAfterFirstUnlockThisDeviceOnly = 6
|
||||
// AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only
|
||||
AccessibleAccessibleAlwaysThisDeviceOnly = 7
|
||||
)
|
||||
|
||||
// MatchLimit is whether to limit results on query
|
||||
type MatchLimit int
|
||||
|
||||
const (
|
||||
// MatchLimitDefault is the default
|
||||
MatchLimitDefault MatchLimit = 0
|
||||
// MatchLimitOne limits to one result
|
||||
MatchLimitOne = 1
|
||||
// MatchLimitAll is no limit
|
||||
MatchLimitAll = 2
|
||||
)
|
||||
|
||||
// MatchLimitKey is key type for MatchLimit
|
||||
var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit))
|
||||
var matchTypeRef = map[MatchLimit]C.CFTypeRef{
|
||||
MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne),
|
||||
MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll),
|
||||
}
|
||||
|
||||
// ReturnAttributesKey is key type for kSecReturnAttributes
|
||||
var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes))
|
||||
|
||||
// ReturnDataKey is key type for kSecReturnData
|
||||
var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData))
|
||||
|
||||
// ReturnRefKey is key type for kSecReturnRef
|
||||
var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef))
|
||||
|
||||
// Item for adding, querying or deleting.
|
||||
type Item struct {
|
||||
// Values can be string, []byte, Convertable or CFTypeRef (constant).
|
||||
attr map[string]interface{}
|
||||
}
|
||||
|
||||
// SetSecClass sets the security class
|
||||
func (k *Item) SetSecClass(sc SecClass) {
|
||||
k.attr[SecClassKey] = secClassTypeRef[sc]
|
||||
}
|
||||
|
||||
// SetInt32 sets an int32 attribute for a string key
|
||||
func (k *Item) SetInt32(key string, v int32) {
|
||||
if v != 0 {
|
||||
k.attr[key] = v
|
||||
} else {
|
||||
delete(k.attr, key)
|
||||
}
|
||||
}
|
||||
|
||||
// SetString sets a string attibute for a string key
|
||||
func (k *Item) SetString(key string, s string) {
|
||||
if s != "" {
|
||||
k.attr[key] = s
|
||||
} else {
|
||||
delete(k.attr, key)
|
||||
}
|
||||
}
|
||||
|
||||
// SetService sets the service attribute (for generic application items)
|
||||
func (k *Item) SetService(s string) {
|
||||
k.SetString(ServiceKey, s)
|
||||
}
|
||||
|
||||
// SetServer sets the server attribute (for internet password items)
|
||||
func (k *Item) SetServer(s string) {
|
||||
k.SetString(ServerKey, s)
|
||||
}
|
||||
|
||||
// SetProtocol sets the protocol attribute (for internet password items)
|
||||
// Example values are: "htps", "http", "smb "
|
||||
func (k *Item) SetProtocol(s string) {
|
||||
k.SetString(ProtocolKey, s)
|
||||
}
|
||||
|
||||
// SetAuthenticationType sets the authentication type attribute (for internet password items)
|
||||
func (k *Item) SetAuthenticationType(s string) {
|
||||
k.SetString(AuthenticationTypeKey, s)
|
||||
}
|
||||
|
||||
// SetPort sets the port attribute (for internet password items)
|
||||
func (k *Item) SetPort(v int32) {
|
||||
k.SetInt32(PortKey, v)
|
||||
}
|
||||
|
||||
// SetPath sets the path attribute (for internet password items)
|
||||
func (k *Item) SetPath(s string) {
|
||||
k.SetString(PathKey, s)
|
||||
}
|
||||
|
||||
// SetAccount sets the account attribute
|
||||
func (k *Item) SetAccount(a string) {
|
||||
k.SetString(AccountKey, a)
|
||||
}
|
||||
|
||||
// SetLabel sets the label attribute
|
||||
func (k *Item) SetLabel(l string) {
|
||||
k.SetString(LabelKey, l)
|
||||
}
|
||||
|
||||
// SetDescription sets the description attribute
|
||||
func (k *Item) SetDescription(s string) {
|
||||
k.SetString(DescriptionKey, s)
|
||||
}
|
||||
|
||||
// SetComment sets the comment attribute
|
||||
func (k *Item) SetComment(s string) {
|
||||
k.SetString(CommentKey, s)
|
||||
}
|
||||
|
||||
// SetData sets the data attribute
|
||||
func (k *Item) SetData(b []byte) {
|
||||
if b != nil {
|
||||
k.attr[DataKey] = b
|
||||
} else {
|
||||
delete(k.attr, DataKey)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessGroup sets the access group attribute
|
||||
func (k *Item) SetAccessGroup(ag string) {
|
||||
k.SetString(AccessGroupKey, ag)
|
||||
}
|
||||
|
||||
// SetSynchronizable sets the synchronizable attribute
|
||||
func (k *Item) SetSynchronizable(sync Synchronizable) {
|
||||
if sync != SynchronizableDefault {
|
||||
k.attr[SynchronizableKey] = syncTypeRef[sync]
|
||||
} else {
|
||||
delete(k.attr, SynchronizableKey)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessible sets the accessible attribute
|
||||
func (k *Item) SetAccessible(accessible Accessible) {
|
||||
if accessible != AccessibleDefault {
|
||||
k.attr[AccessibleKey] = accessibleTypeRef[accessible]
|
||||
} else {
|
||||
delete(k.attr, AccessibleKey)
|
||||
}
|
||||
}
|
||||
|
||||
// SetMatchLimit sets the match limit
|
||||
func (k *Item) SetMatchLimit(matchLimit MatchLimit) {
|
||||
if matchLimit != MatchLimitDefault {
|
||||
k.attr[MatchLimitKey] = matchTypeRef[matchLimit]
|
||||
} else {
|
||||
delete(k.attr, MatchLimitKey)
|
||||
}
|
||||
}
|
||||
|
||||
// SetReturnAttributes sets the return value type on query
|
||||
func (k *Item) SetReturnAttributes(b bool) {
|
||||
k.attr[ReturnAttributesKey] = b
|
||||
}
|
||||
|
||||
// SetReturnData enables returning data on query
|
||||
func (k *Item) SetReturnData(b bool) {
|
||||
k.attr[ReturnDataKey] = b
|
||||
}
|
||||
|
||||
// SetReturnRef enables returning references on query
|
||||
func (k *Item) SetReturnRef(b bool) {
|
||||
k.attr[ReturnRefKey] = b
|
||||
}
|
||||
|
||||
// NewItem is a new empty keychain item
|
||||
func NewItem() Item {
|
||||
return Item{make(map[string]interface{})}
|
||||
}
|
||||
|
||||
// NewGenericPassword creates a generic password item with the default keychain. This is a convenience method.
|
||||
func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item {
|
||||
item := NewItem()
|
||||
item.SetSecClass(SecClassGenericPassword)
|
||||
item.SetService(service)
|
||||
item.SetAccount(account)
|
||||
item.SetLabel(label)
|
||||
item.SetData(data)
|
||||
item.SetAccessGroup(accessGroup)
|
||||
return item
|
||||
}
|
||||
|
||||
// AddItem adds a Item to a Keychain
|
||||
func AddItem(item Item) error {
|
||||
cfDict, err := ConvertMapToCFDictionary(item.attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer Release(C.CFTypeRef(cfDict))
|
||||
|
||||
errCode := C.SecItemAdd(cfDict, nil)
|
||||
err = checkError(errCode)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateItem updates the queryItem with the parameters from updateItem
|
||||
func UpdateItem(queryItem Item, updateItem Item) error {
|
||||
cfDict, err := ConvertMapToCFDictionary(queryItem.attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer Release(C.CFTypeRef(cfDict))
|
||||
cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer Release(C.CFTypeRef(cfDictUpdate))
|
||||
errCode := C.SecItemUpdate(cfDict, cfDictUpdate)
|
||||
err = checkError(errCode)
|
||||
return err
|
||||
}
|
||||
|
||||
// QueryResult stores all possible results from queries.
|
||||
// Not all fields are applicable all the time. Results depend on query.
|
||||
type QueryResult struct {
|
||||
// For generic application items
|
||||
Service string
|
||||
|
||||
// For internet password items
|
||||
Server string
|
||||
Protocol string
|
||||
AuthenticationType string
|
||||
Port int32
|
||||
Path string
|
||||
|
||||
Account string
|
||||
AccessGroup string
|
||||
Label string
|
||||
Description string
|
||||
Comment string
|
||||
Data []byte
|
||||
CreationDate time.Time
|
||||
ModificationDate time.Time
|
||||
}
|
||||
|
||||
// QueryItemRef returns query result as CFTypeRef. You must release it when you are done.
|
||||
func QueryItemRef(item Item) (C.CFTypeRef, error) {
|
||||
cfDict, err := ConvertMapToCFDictionary(item.attr)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer Release(C.CFTypeRef(cfDict))
|
||||
|
||||
var resultsRef C.CFTypeRef
|
||||
errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint
|
||||
if Error(errCode) == ErrorItemNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
err = checkError(errCode)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return resultsRef, nil
|
||||
}
|
||||
|
||||
// QueryItem returns a list of query results.
|
||||
func QueryItem(item Item) ([]QueryResult, error) {
|
||||
resultsRef, err := QueryItemRef(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resultsRef == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
defer Release(resultsRef)
|
||||
|
||||
results := make([]QueryResult, 0, 1)
|
||||
|
||||
typeID := C.CFGetTypeID(resultsRef)
|
||||
if typeID == C.CFArrayGetTypeID() {
|
||||
arr := CFArrayToArray(C.CFArrayRef(resultsRef))
|
||||
for _, ref := range arr {
|
||||
elementTypeID := C.CFGetTypeID(ref)
|
||||
if elementTypeID == C.CFDictionaryGetTypeID() {
|
||||
item, err := convertResult(C.CFDictionaryRef(ref))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, *item)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)")
|
||||
}
|
||||
}
|
||||
} else if typeID == C.CFDictionaryGetTypeID() {
|
||||
item, err := convertResult(C.CFDictionaryRef(resultsRef))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, *item)
|
||||
} else if typeID == C.CFDataGetTypeID() {
|
||||
b, err := CFDataToBytes(C.CFDataRef(resultsRef))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := QueryResult{Data: b}
|
||||
results = append(results, item)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func attrKey(ref C.CFTypeRef) string {
|
||||
return CFStringToString(C.CFStringRef(ref))
|
||||
}
|
||||
|
||||
func convertResult(d C.CFDictionaryRef) (*QueryResult, error) {
|
||||
m := CFDictionaryToMap(d)
|
||||
result := QueryResult{}
|
||||
for k, v := range m {
|
||||
switch attrKey(k) {
|
||||
case ServiceKey:
|
||||
result.Service = CFStringToString(C.CFStringRef(v))
|
||||
case ServerKey:
|
||||
result.Server = CFStringToString(C.CFStringRef(v))
|
||||
case ProtocolKey:
|
||||
result.Protocol = CFStringToString(C.CFStringRef(v))
|
||||
case AuthenticationTypeKey:
|
||||
result.AuthenticationType = CFStringToString(C.CFStringRef(v))
|
||||
case PortKey:
|
||||
val := CFNumberToInterface(C.CFNumberRef(v))
|
||||
result.Port = val.(int32)
|
||||
case PathKey:
|
||||
result.Path = CFStringToString(C.CFStringRef(v))
|
||||
case AccountKey:
|
||||
result.Account = CFStringToString(C.CFStringRef(v))
|
||||
case AccessGroupKey:
|
||||
result.AccessGroup = CFStringToString(C.CFStringRef(v))
|
||||
case LabelKey:
|
||||
result.Label = CFStringToString(C.CFStringRef(v))
|
||||
case DescriptionKey:
|
||||
result.Description = CFStringToString(C.CFStringRef(v))
|
||||
case CommentKey:
|
||||
result.Comment = CFStringToString(C.CFStringRef(v))
|
||||
case DataKey:
|
||||
b, err := CFDataToBytes(C.CFDataRef(v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Data = b
|
||||
case CreationDateKey:
|
||||
result.CreationDate = CFDateToTime(C.CFDateRef(v))
|
||||
case ModificationDateKey:
|
||||
result.ModificationDate = CFDateToTime(C.CFDateRef(v))
|
||||
// default:
|
||||
// fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v))
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteGenericPasswordItem removes a generic password item.
|
||||
func DeleteGenericPasswordItem(service string, account string) error {
|
||||
item := NewItem()
|
||||
item.SetSecClass(SecClassGenericPassword)
|
||||
item.SetService(service)
|
||||
item.SetAccount(account)
|
||||
return DeleteItem(item)
|
||||
}
|
||||
|
||||
// DeleteItem removes a Item
|
||||
func DeleteItem(item Item) error {
|
||||
cfDict, err := ConvertMapToCFDictionary(item.attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer Release(C.CFTypeRef(cfDict))
|
||||
|
||||
errCode := C.SecItemDelete(cfDict)
|
||||
return checkError(errCode)
|
||||
}
|
||||
|
||||
// GetAccountsForService is deprecated
|
||||
func GetAccountsForService(service string) ([]string, error) {
|
||||
return GetGenericPasswordAccounts(service)
|
||||
}
|
||||
|
||||
// GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method.
|
||||
func GetGenericPasswordAccounts(service string) ([]string, error) {
|
||||
query := NewItem()
|
||||
query.SetSecClass(SecClassGenericPassword)
|
||||
query.SetService(service)
|
||||
query.SetMatchLimit(MatchLimitAll)
|
||||
query.SetReturnAttributes(true)
|
||||
results, err := QueryItem(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accounts := make([]string, 0, len(results))
|
||||
for _, r := range results {
|
||||
accounts = append(accounts, r.Account)
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetGenericPassword returns password data for service and account. This is a convenience method.
|
||||
// If item is not found returns nil, nil.
|
||||
func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) {
|
||||
query := NewItem()
|
||||
query.SetSecClass(SecClassGenericPassword)
|
||||
query.SetService(service)
|
||||
query.SetAccount(account)
|
||||
query.SetLabel(label)
|
||||
query.SetAccessGroup(accessGroup)
|
||||
query.SetMatchLimit(MatchLimitOne)
|
||||
query.SetReturnData(true)
|
||||
results, err := QueryItem(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(results) > 1 {
|
||||
return nil, fmt.Errorf("Too many results")
|
||||
}
|
||||
if len(results) == 1 {
|
||||
return results[0].Data, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
//go:build darwin && !ios
|
||||
// +build darwin,!ios
|
||||
|
||||
package keychain
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -framework CoreFoundation -framework Security
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <Security/Security.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// AccessibleKey is key for kSecAttrAccessible
|
||||
var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible))
|
||||
var accessibleTypeRef = map[Accessible]C.CFTypeRef{
|
||||
AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked),
|
||||
AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock),
|
||||
AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways),
|
||||
AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly),
|
||||
AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly),
|
||||
AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly),
|
||||
|
||||
// Only available in 10.10
|
||||
//AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly),
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package keychain
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var randRead = rand.Read
|
||||
|
||||
// RandomID returns random ID (base32) string with prefix, using 256 bits as
|
||||
// recommended by tptacek: https://gist.github.com/tqbf/be58d2d39690c3b366ad
|
||||
func RandomID(prefix string) (string, error) {
|
||||
buf, err := RandBytes(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
str := base32.StdEncoding.EncodeToString(buf)
|
||||
str = strings.ReplaceAll(str, "=", "")
|
||||
str = prefix + str
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// RandBytes returns random bytes of length
|
||||
func RandBytes(length int) ([]byte, error) {
|
||||
buf := make([]byte, length)
|
||||
if _, err := randRead(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package execabs is a drop-in replacement for os/exec
|
||||
// that requires PATH lookups to find absolute paths.
|
||||
// That is, execabs.Command("cmd") runs the same PATH lookup
|
||||
// as exec.Command("cmd"), but if the result is a path
|
||||
// which is relative, the Run and Start methods will report
|
||||
// an error instead of running the executable.
|
||||
//
|
||||
// See https://blog.golang.org/path-security for more information
|
||||
// about when it may be necessary or appropriate to use this package.
|
||||
package execabs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ErrNotFound is the error resulting if a path search failed to find an executable file.
|
||||
// It is an alias for exec.ErrNotFound.
|
||||
var ErrNotFound = exec.ErrNotFound
|
||||
|
||||
// Cmd represents an external command being prepared or run.
|
||||
// It is an alias for exec.Cmd.
|
||||
type Cmd = exec.Cmd
|
||||
|
||||
// Error is returned by LookPath when it fails to classify a file as an executable.
|
||||
// It is an alias for exec.Error.
|
||||
type Error = exec.Error
|
||||
|
||||
// An ExitError reports an unsuccessful exit by a command.
|
||||
// It is an alias for exec.ExitError.
|
||||
type ExitError = exec.ExitError
|
||||
|
||||
func relError(file, path string) error {
|
||||
return fmt.Errorf("%s resolves to executable in current directory (.%c%s)", file, filepath.Separator, path)
|
||||
}
|
||||
|
||||
// LookPath searches for an executable named file in the directories
|
||||
// named by the PATH environment variable. If file contains a slash,
|
||||
// it is tried directly and the PATH is not consulted. The result will be
|
||||
// an absolute path.
|
||||
//
|
||||
// LookPath differs from exec.LookPath in its handling of PATH lookups,
|
||||
// which are used for file names without slashes. If exec.LookPath's
|
||||
// PATH lookup would have returned an executable from the current directory,
|
||||
// LookPath instead returns an error.
|
||||
func LookPath(file string) (string, error) {
|
||||
path, err := exec.LookPath(file)
|
||||
if err != nil && !isGo119ErrDot(err) {
|
||||
return "", err
|
||||
}
|
||||
if filepath.Base(file) == file && !filepath.IsAbs(path) {
|
||||
return "", relError(file, path)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func fixCmd(name string, cmd *exec.Cmd) {
|
||||
if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) {
|
||||
// exec.Command was called with a bare binary name and
|
||||
// exec.LookPath returned a path which is not absolute.
|
||||
// Set cmd.lookPathErr and clear cmd.Path so that it
|
||||
// cannot be run.
|
||||
lookPathErr := (*error)(unsafe.Pointer(reflect.ValueOf(cmd).Elem().FieldByName("lookPathErr").Addr().Pointer()))
|
||||
if *lookPathErr == nil {
|
||||
*lookPathErr = relError(name, cmd.Path)
|
||||
}
|
||||
cmd.Path = ""
|
||||
}
|
||||
}
|
||||
|
||||
// CommandContext is like Command but includes a context.
|
||||
//
|
||||
// The provided context is used to kill the process (by calling os.Process.Kill)
|
||||
// if the context becomes done before the command completes on its own.
|
||||
func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
fixCmd(name, cmd)
|
||||
return cmd
|
||||
|
||||
}
|
||||
|
||||
// Command returns the Cmd struct to execute the named program with the given arguments.
|
||||
// See exec.Command for most details.
|
||||
//
|
||||
// Command differs from exec.Command in its handling of PATH lookups,
|
||||
// which are used when the program name contains no slashes.
|
||||
// If exec.Command would have returned an exec.Cmd configured to run an
|
||||
// executable from the current directory, Command instead
|
||||
// returns an exec.Cmd that will return an error from Start or Run.
|
||||
func Command(name string, arg ...string) *exec.Cmd {
|
||||
cmd := exec.Command(name, arg...)
|
||||
fixCmd(name, cmd)
|
||||
return cmd
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.19
|
||||
// +build !go1.19
|
||||
|
||||
package execabs
|
||||
|
||||
func isGo119ErrDot(err error) bool {
|
||||
return false
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.19
|
||||
// +build go1.19
|
||||
|
||||
package execabs
|
||||
|
||||
import "strings"
|
||||
|
||||
func isGo119ErrDot(err error) bool {
|
||||
// TODO: return errors.Is(err, exec.ErrDot)
|
||||
return strings.Contains(err.Error(), "current directory")
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package unsafeheader contains header declarations for the Go runtime's
|
||||
// slice and string implementations.
|
||||
//
|
||||
// This package allows x/sys to use types equivalent to
|
||||
// reflect.SliceHeader and reflect.StringHeader without introducing
|
||||
// a dependency on the (relatively heavy) "reflect" package.
|
||||
package unsafeheader
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Slice is the runtime representation of a slice.
|
||||
// It cannot be used safely or portably and its representation may change in a later release.
|
||||
type Slice struct {
|
||||
Data unsafe.Pointer
|
||||
Len int
|
||||
Cap int
|
||||
}
|
||||
|
||||
// String is the runtime representation of a string.
|
||||
// It cannot be used safely or portably and its representation may change in a later release.
|
||||
type String struct {
|
||||
Data unsafe.Pointer
|
||||
Len int
|
||||
}
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && go1.9
|
||||
// +build windows,go1.9
|
||||
//go:build windows
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.12
|
||||
// +build !go1.12
|
||||
|
||||
// This file is here to allow bodyless functions with go:linkname for Go 1.11
|
||||
// and earlier (see https://golang.org/issue/23311).
|
||||
+10
-7
@@ -37,14 +37,17 @@ func (token Token) Environ(inheritExisting bool) (env []string, err error) {
|
||||
return nil, err
|
||||
}
|
||||
defer DestroyEnvironmentBlock(block)
|
||||
blockp := uintptr(unsafe.Pointer(block))
|
||||
for {
|
||||
entry := UTF16PtrToString((*uint16)(unsafe.Pointer(blockp)))
|
||||
if len(entry) == 0 {
|
||||
break
|
||||
size := unsafe.Sizeof(*block)
|
||||
for *block != 0 {
|
||||
// find NUL terminator
|
||||
end := unsafe.Pointer(block)
|
||||
for *(*uint16)(end) != 0 {
|
||||
end = unsafe.Add(end, size)
|
||||
}
|
||||
env = append(env, entry)
|
||||
blockp += 2 * (uintptr(len(entry)) + 1)
|
||||
|
||||
entry := unsafe.Slice(block, (uintptr(end)-uintptr(unsafe.Pointer(block)))/size)
|
||||
env = append(env, UTF16ToString(entry))
|
||||
block = (*uint16)(unsafe.Add(end, size))
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
+82
-12
@@ -22,7 +22,7 @@ import (
|
||||
// but only if there is space or tab inside s.
|
||||
func EscapeArg(s string) string {
|
||||
if len(s) == 0 {
|
||||
return "\"\""
|
||||
return `""`
|
||||
}
|
||||
n := len(s)
|
||||
hasSpace := false
|
||||
@@ -35,7 +35,7 @@ func EscapeArg(s string) string {
|
||||
}
|
||||
}
|
||||
if hasSpace {
|
||||
n += 2
|
||||
n += 2 // Reserve space for quotes.
|
||||
}
|
||||
if n == len(s) {
|
||||
return s
|
||||
@@ -82,36 +82,106 @@ func EscapeArg(s string) string {
|
||||
// in CreateProcess's CommandLine argument, CreateService/ChangeServiceConfig's BinaryPathName argument,
|
||||
// or any program that uses CommandLineToArgv.
|
||||
func ComposeCommandLine(args []string) string {
|
||||
var commandLine string
|
||||
for i := range args {
|
||||
if i > 0 {
|
||||
commandLine += " "
|
||||
}
|
||||
commandLine += EscapeArg(args[i])
|
||||
if len(args) == 0 {
|
||||
return ""
|
||||
}
|
||||
return commandLine
|
||||
|
||||
// Per https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw:
|
||||
// “This function accepts command lines that contain a program name; the
|
||||
// program name can be enclosed in quotation marks or not.”
|
||||
//
|
||||
// Unfortunately, it provides no means of escaping interior quotation marks
|
||||
// within that program name, and we have no way to report them here.
|
||||
prog := args[0]
|
||||
mustQuote := len(prog) == 0
|
||||
for i := 0; i < len(prog); i++ {
|
||||
c := prog[i]
|
||||
if c <= ' ' || (c == '"' && i == 0) {
|
||||
// Force quotes for not only the ASCII space and tab as described in the
|
||||
// MSDN article, but also ASCII control characters.
|
||||
// The documentation for CommandLineToArgvW doesn't say what happens when
|
||||
// the first argument is not a valid program name, but it empirically
|
||||
// seems to drop unquoted control characters.
|
||||
mustQuote = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var commandLine []byte
|
||||
if mustQuote {
|
||||
commandLine = make([]byte, 0, len(prog)+2)
|
||||
commandLine = append(commandLine, '"')
|
||||
for i := 0; i < len(prog); i++ {
|
||||
c := prog[i]
|
||||
if c == '"' {
|
||||
// This quote would interfere with our surrounding quotes.
|
||||
// We have no way to report an error, so just strip out
|
||||
// the offending character instead.
|
||||
continue
|
||||
}
|
||||
commandLine = append(commandLine, c)
|
||||
}
|
||||
commandLine = append(commandLine, '"')
|
||||
} else {
|
||||
if len(args) == 1 {
|
||||
// args[0] is a valid command line representing itself.
|
||||
// No need to allocate a new slice or string for it.
|
||||
return prog
|
||||
}
|
||||
commandLine = []byte(prog)
|
||||
}
|
||||
|
||||
for _, arg := range args[1:] {
|
||||
commandLine = append(commandLine, ' ')
|
||||
// TODO(bcmills): since we're already appending to a slice, it would be nice
|
||||
// to avoid the intermediate allocations of EscapeArg.
|
||||
// Perhaps we can factor out an appendEscapedArg function.
|
||||
commandLine = append(commandLine, EscapeArg(arg)...)
|
||||
}
|
||||
return string(commandLine)
|
||||
}
|
||||
|
||||
// DecomposeCommandLine breaks apart its argument command line into unescaped parts using CommandLineToArgv,
|
||||
// as gathered from GetCommandLine, QUERY_SERVICE_CONFIG's BinaryPathName argument, or elsewhere that
|
||||
// command lines are passed around.
|
||||
// DecomposeCommandLine returns an error if commandLine contains NUL.
|
||||
func DecomposeCommandLine(commandLine string) ([]string, error) {
|
||||
if len(commandLine) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
utf16CommandLine, err := UTF16FromString(commandLine)
|
||||
if err != nil {
|
||||
return nil, errorspkg.New("string with NUL passed to DecomposeCommandLine")
|
||||
}
|
||||
var argc int32
|
||||
argv, err := CommandLineToArgv(StringToUTF16Ptr(commandLine), &argc)
|
||||
argv, err := commandLineToArgv(&utf16CommandLine[0], &argc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer LocalFree(Handle(unsafe.Pointer(argv)))
|
||||
|
||||
var args []string
|
||||
for _, v := range (*argv)[:argc] {
|
||||
args = append(args, UTF16ToString((*v)[:]))
|
||||
for _, p := range unsafe.Slice(argv, argc) {
|
||||
args = append(args, UTF16PtrToString(p))
|
||||
}
|
||||
return args, nil
|
||||
}
|
||||
|
||||
// CommandLineToArgv parses a Unicode command line string and sets
|
||||
// argc to the number of parsed arguments.
|
||||
//
|
||||
// The returned memory should be freed using a single call to LocalFree.
|
||||
//
|
||||
// Note that although the return type of CommandLineToArgv indicates 8192
|
||||
// entries of up to 8192 characters each, the actual count of parsed arguments
|
||||
// may exceed 8192, and the documentation for CommandLineToArgvW does not mention
|
||||
// any bound on the lengths of the individual argument strings.
|
||||
// (See https://go.dev/issue/63236.)
|
||||
func CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) {
|
||||
argp, err := commandLineToArgv(cmd, argc)
|
||||
argv = (*[8192]*[8192]uint16)(unsafe.Pointer(argp))
|
||||
return argv, err
|
||||
}
|
||||
|
||||
func CloseOnExec(fd Handle) {
|
||||
SetHandleInformation(Handle(fd), HANDLE_FLAG_INHERIT, 0)
|
||||
}
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build generate
|
||||
// +build generate
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && race
|
||||
// +build windows,race
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && !race
|
||||
// +build windows,!race
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
+6
-15
@@ -7,8 +7,6 @@ package windows
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/internal/unsafeheader"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1341,21 +1339,14 @@ func (selfRelativeSD *SECURITY_DESCRIPTOR) copySelfRelativeSecurityDescriptor()
|
||||
sdLen = min
|
||||
}
|
||||
|
||||
var src []byte
|
||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&src))
|
||||
h.Data = unsafe.Pointer(selfRelativeSD)
|
||||
h.Len = sdLen
|
||||
h.Cap = sdLen
|
||||
|
||||
src := unsafe.Slice((*byte)(unsafe.Pointer(selfRelativeSD)), sdLen)
|
||||
// SECURITY_DESCRIPTOR has pointers in it, which means checkptr expects for it to
|
||||
// be aligned properly. When we're copying a Windows-allocated struct to a
|
||||
// Go-allocated one, make sure that the Go allocation is aligned to the
|
||||
// pointer size.
|
||||
const psize = int(unsafe.Sizeof(uintptr(0)))
|
||||
|
||||
var dst []byte
|
||||
h = (*unsafeheader.Slice)(unsafe.Pointer(&dst))
|
||||
alloc := make([]uintptr, (sdLen+psize-1)/psize)
|
||||
h.Data = (*unsafeheader.Slice)(unsafe.Pointer(&alloc)).Data
|
||||
h.Len = sdLen
|
||||
h.Cap = sdLen
|
||||
|
||||
dst := unsafe.Slice((*byte)(unsafe.Pointer(&alloc[0])), sdLen)
|
||||
copy(dst, src)
|
||||
return (*SECURITY_DESCRIPTOR)(unsafe.Pointer(&dst[0]))
|
||||
}
|
||||
|
||||
+11
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package windows
|
||||
|
||||
@@ -141,6 +140,12 @@ const (
|
||||
SERVICE_DYNAMIC_INFORMATION_LEVEL_START_REASON = 1
|
||||
)
|
||||
|
||||
type ENUM_SERVICE_STATUS struct {
|
||||
ServiceName *uint16
|
||||
DisplayName *uint16
|
||||
ServiceStatus SERVICE_STATUS
|
||||
}
|
||||
|
||||
type SERVICE_STATUS struct {
|
||||
ServiceType uint32
|
||||
CurrentState uint32
|
||||
@@ -212,6 +217,10 @@ type SERVICE_FAILURE_ACTIONS struct {
|
||||
Actions *SC_ACTION
|
||||
}
|
||||
|
||||
type SERVICE_FAILURE_ACTIONS_FLAG struct {
|
||||
FailureActionsOnNonCrashFailures int32
|
||||
}
|
||||
|
||||
type SC_ACTION struct {
|
||||
Type uint32
|
||||
Delay uint32
|
||||
@@ -245,3 +254,4 @@ type QUERY_SERVICE_LOCK_STATUS struct {
|
||||
//sys UnsubscribeServiceChangeNotifications(subscription uintptr) = sechost.UnsubscribeServiceChangeNotifications?
|
||||
//sys RegisterServiceCtrlHandlerEx(serviceName *uint16, handlerProc uintptr, context uintptr) (handle Handle, err error) = advapi32.RegisterServiceCtrlHandlerExW
|
||||
//sys QueryServiceDynamicInformation(service Handle, infoLevel uint32, dynamicInfo unsafe.Pointer) (err error) = advapi32.QueryServiceDynamicInformation?
|
||||
//sys EnumDependentServices(service Handle, activityState uint32, services *ENUM_SERVICE_STATUS, buffSize uint32, bytesNeeded *uint32, servicesReturned *uint32) (err error) = advapi32.EnumDependentServicesW
|
||||
|
||||
-1
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package windows
|
||||
|
||||
|
||||
+1
-10
@@ -3,7 +3,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
// Package windows contains an interface to the low-level operating system
|
||||
// primitives. OS details vary depending on the underlying system, and
|
||||
@@ -30,8 +29,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/internal/unsafeheader"
|
||||
)
|
||||
|
||||
// ByteSliceFromString returns a NUL-terminated slice of bytes
|
||||
@@ -83,13 +80,7 @@ func BytePtrToString(p *byte) string {
|
||||
ptr = unsafe.Pointer(uintptr(ptr) + 1)
|
||||
}
|
||||
|
||||
var s []byte
|
||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&s))
|
||||
h.Data = unsafe.Pointer(p)
|
||||
h.Len = n
|
||||
h.Cap = n
|
||||
|
||||
return string(s)
|
||||
return string(unsafe.Slice(p, n))
|
||||
}
|
||||
|
||||
// Single-word zero for use when we need a valid pointer to 0 bytes.
|
||||
|
||||
+165
-51
@@ -10,14 +10,11 @@ import (
|
||||
errorspkg "errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/internal/unsafeheader"
|
||||
)
|
||||
|
||||
type Handle uintptr
|
||||
@@ -87,22 +84,13 @@ func StringToUTF16(s string) []uint16 {
|
||||
// s, with a terminating NUL added. If s contains a NUL byte at any
|
||||
// location, it returns (nil, syscall.EINVAL).
|
||||
func UTF16FromString(s string) ([]uint16, error) {
|
||||
if strings.IndexByte(s, 0) != -1 {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
return utf16.Encode([]rune(s + "\x00")), nil
|
||||
return syscall.UTF16FromString(s)
|
||||
}
|
||||
|
||||
// UTF16ToString returns the UTF-8 encoding of the UTF-16 sequence s,
|
||||
// with a terminating NUL and any bytes after the NUL removed.
|
||||
func UTF16ToString(s []uint16) string {
|
||||
for i, v := range s {
|
||||
if v == 0 {
|
||||
s = s[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
return string(utf16.Decode(s))
|
||||
return syscall.UTF16ToString(s)
|
||||
}
|
||||
|
||||
// StringToUTF16Ptr is deprecated. Use UTF16PtrFromString instead.
|
||||
@@ -137,28 +125,21 @@ func UTF16PtrToString(p *uint16) string {
|
||||
for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ {
|
||||
ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*p))
|
||||
}
|
||||
|
||||
var s []uint16
|
||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&s))
|
||||
h.Data = unsafe.Pointer(p)
|
||||
h.Len = n
|
||||
h.Cap = n
|
||||
|
||||
return string(utf16.Decode(s))
|
||||
return UTF16ToString(unsafe.Slice(p, n))
|
||||
}
|
||||
|
||||
func Getpagesize() int { return 4096 }
|
||||
|
||||
// NewCallback converts a Go function to a function pointer conforming to the stdcall calling convention.
|
||||
// This is useful when interoperating with Windows code requiring callbacks.
|
||||
// The argument is expected to be a function with with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||
// The argument is expected to be a function with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||
func NewCallback(fn interface{}) uintptr {
|
||||
return syscall.NewCallback(fn)
|
||||
}
|
||||
|
||||
// NewCallbackCDecl converts a Go function to a function pointer conforming to the cdecl calling convention.
|
||||
// This is useful when interoperating with Windows code requiring callbacks.
|
||||
// The argument is expected to be a function with with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||
// The argument is expected to be a function with one uintptr-sized result. The function must not have arguments with size larger than the size of uintptr.
|
||||
func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
return syscall.NewCallbackCDecl(fn)
|
||||
}
|
||||
@@ -173,6 +154,8 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys GetModuleFileName(module Handle, filename *uint16, size uint32) (n uint32, err error) = kernel32.GetModuleFileNameW
|
||||
//sys GetModuleHandleEx(flags uint32, moduleName *uint16, module *Handle) (err error) = kernel32.GetModuleHandleExW
|
||||
//sys SetDefaultDllDirectories(directoryFlags uint32) (err error)
|
||||
//sys AddDllDirectory(path *uint16) (cookie uintptr, err error) = kernel32.AddDllDirectory
|
||||
//sys RemoveDllDirectory(cookie uintptr) (err error) = kernel32.RemoveDllDirectory
|
||||
//sys SetDllDirectory(path string) (err error) = kernel32.SetDllDirectoryW
|
||||
//sys GetVersion() (ver uint32, err error)
|
||||
//sys FormatMessage(flags uint32, msgsrc uintptr, msgid uint32, langid uint32, buf []uint16, args *byte) (n uint32, err error) = FormatMessageW
|
||||
@@ -182,6 +165,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys CreateFile(name *uint16, access uint32, mode uint32, sa *SecurityAttributes, createmode uint32, attrs uint32, templatefile Handle) (handle Handle, err error) [failretval==InvalidHandle] = CreateFileW
|
||||
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *SecurityAttributes) (handle Handle, err error) [failretval==InvalidHandle] = CreateNamedPipeW
|
||||
//sys ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error)
|
||||
//sys DisconnectNamedPipe(pipe Handle) (err error)
|
||||
//sys GetNamedPipeInfo(pipe Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error)
|
||||
//sys GetNamedPipeHandleState(pipe Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
|
||||
//sys SetNamedPipeHandleState(pipe Handle, state *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32) (err error) = SetNamedPipeHandleState
|
||||
@@ -210,6 +194,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys GetComputerName(buf *uint16, n *uint32) (err error) = GetComputerNameW
|
||||
//sys GetComputerNameEx(nametype uint32, buf *uint16, n *uint32) (err error) = GetComputerNameExW
|
||||
//sys SetEndOfFile(handle Handle) (err error)
|
||||
//sys SetFileValidData(handle Handle, validDataLength int64) (err error)
|
||||
//sys GetSystemTimeAsFileTime(time *Filetime)
|
||||
//sys GetSystemTimePreciseAsFileTime(time *Filetime)
|
||||
//sys GetTimeZoneInformation(tzi *Timezoneinformation) (rc uint32, err error) [failretval==0xffffffff]
|
||||
@@ -232,7 +217,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys shGetKnownFolderPath(id *KNOWNFOLDERID, flags uint32, token Token, path **uint16) (ret error) = shell32.SHGetKnownFolderPath
|
||||
//sys TerminateProcess(handle Handle, exitcode uint32) (err error)
|
||||
//sys GetExitCodeProcess(handle Handle, exitcode *uint32) (err error)
|
||||
//sys GetStartupInfo(startupInfo *StartupInfo) (err error) = GetStartupInfoW
|
||||
//sys getStartupInfo(startupInfo *StartupInfo) = GetStartupInfoW
|
||||
//sys GetProcessTimes(handle Handle, creationTime *Filetime, exitTime *Filetime, kernelTime *Filetime, userTime *Filetime) (err error)
|
||||
//sys DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error)
|
||||
//sys WaitForSingleObject(handle Handle, waitMilliseconds uint32) (event uint32, err error) [failretval==0xffffffff]
|
||||
@@ -251,12 +236,13 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys CreateEnvironmentBlock(block **uint16, token Token, inheritExisting bool) (err error) = userenv.CreateEnvironmentBlock
|
||||
//sys DestroyEnvironmentBlock(block *uint16) (err error) = userenv.DestroyEnvironmentBlock
|
||||
//sys getTickCount64() (ms uint64) = kernel32.GetTickCount64
|
||||
//sys GetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error)
|
||||
//sys SetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error)
|
||||
//sys GetFileAttributes(name *uint16) (attrs uint32, err error) [failretval==INVALID_FILE_ATTRIBUTES] = kernel32.GetFileAttributesW
|
||||
//sys SetFileAttributes(name *uint16, attrs uint32) (err error) = kernel32.SetFileAttributesW
|
||||
//sys GetFileAttributesEx(name *uint16, level uint32, info *byte) (err error) = kernel32.GetFileAttributesExW
|
||||
//sys GetCommandLine() (cmd *uint16) = kernel32.GetCommandLineW
|
||||
//sys CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) [failretval==nil] = shell32.CommandLineToArgvW
|
||||
//sys commandLineToArgv(cmd *uint16, argc *int32) (argv **uint16, err error) [failretval==nil] = shell32.CommandLineToArgvW
|
||||
//sys LocalFree(hmem Handle) (handle Handle, err error) [failretval!=0]
|
||||
//sys LocalAlloc(flags uint32, length uint32) (ptr uintptr, err error)
|
||||
//sys SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error)
|
||||
@@ -315,12 +301,15 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys RegNotifyChangeKeyValue(key Handle, watchSubtree bool, notifyFilter uint32, event Handle, asynchronous bool) (regerrno error) = advapi32.RegNotifyChangeKeyValue
|
||||
//sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId
|
||||
//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId
|
||||
//sys ClosePseudoConsole(console Handle) = kernel32.ClosePseudoConsole
|
||||
//sys createPseudoConsole(size uint32, in Handle, out Handle, flags uint32, pconsole *Handle) (hr error) = kernel32.CreatePseudoConsole
|
||||
//sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode
|
||||
//sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode
|
||||
//sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo
|
||||
//sys setConsoleCursorPosition(console Handle, position uint32) (err error) = kernel32.SetConsoleCursorPosition
|
||||
//sys WriteConsole(console Handle, buf *uint16, towrite uint32, written *uint32, reserved *byte) (err error) = kernel32.WriteConsoleW
|
||||
//sys ReadConsole(console Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) (err error) = kernel32.ReadConsoleW
|
||||
//sys resizePseudoConsole(pconsole Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
|
||||
//sys CreateToolhelp32Snapshot(flags uint32, processId uint32) (handle Handle, err error) [failretval==InvalidHandle] = kernel32.CreateToolhelp32Snapshot
|
||||
//sys Module32First(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32FirstW
|
||||
//sys Module32Next(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32NextW
|
||||
@@ -360,10 +349,31 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys SetProcessPriorityBoost(process Handle, disable bool) (err error) = kernel32.SetProcessPriorityBoost
|
||||
//sys GetProcessWorkingSetSizeEx(hProcess Handle, lpMinimumWorkingSetSize *uintptr, lpMaximumWorkingSetSize *uintptr, flags *uint32)
|
||||
//sys SetProcessWorkingSetSizeEx(hProcess Handle, dwMinimumWorkingSetSize uintptr, dwMaximumWorkingSetSize uintptr, flags uint32) (err error)
|
||||
//sys ClearCommBreak(handle Handle) (err error)
|
||||
//sys ClearCommError(handle Handle, lpErrors *uint32, lpStat *ComStat) (err error)
|
||||
//sys EscapeCommFunction(handle Handle, dwFunc uint32) (err error)
|
||||
//sys GetCommState(handle Handle, lpDCB *DCB) (err error)
|
||||
//sys GetCommModemStatus(handle Handle, lpModemStat *uint32) (err error)
|
||||
//sys GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
||||
//sys PurgeComm(handle Handle, dwFlags uint32) (err error)
|
||||
//sys SetCommBreak(handle Handle) (err error)
|
||||
//sys SetCommMask(handle Handle, dwEvtMask uint32) (err error)
|
||||
//sys SetCommState(handle Handle, lpDCB *DCB) (err error)
|
||||
//sys SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error)
|
||||
//sys SetupComm(handle Handle, dwInQueue uint32, dwOutQueue uint32) (err error)
|
||||
//sys WaitCommEvent(handle Handle, lpEvtMask *uint32, lpOverlapped *Overlapped) (err error)
|
||||
//sys GetActiveProcessorCount(groupNumber uint16) (ret uint32)
|
||||
//sys GetMaximumProcessorCount(groupNumber uint16) (ret uint32)
|
||||
//sys EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) = user32.EnumWindows
|
||||
//sys EnumChildWindows(hwnd HWND, enumFunc uintptr, param unsafe.Pointer) = user32.EnumChildWindows
|
||||
//sys GetClassName(hwnd HWND, className *uint16, maxCount int32) (copied int32, err error) = user32.GetClassNameW
|
||||
//sys GetDesktopWindow() (hwnd HWND) = user32.GetDesktopWindow
|
||||
//sys GetForegroundWindow() (hwnd HWND) = user32.GetForegroundWindow
|
||||
//sys IsWindow(hwnd HWND) (isWindow bool) = user32.IsWindow
|
||||
//sys IsWindowUnicode(hwnd HWND) (isUnicode bool) = user32.IsWindowUnicode
|
||||
//sys IsWindowVisible(hwnd HWND) (isVisible bool) = user32.IsWindowVisible
|
||||
//sys GetGUIThreadInfo(thread uint32, info *GUIThreadInfo) (err error) = user32.GetGUIThreadInfo
|
||||
//sys GetLargePageMinimum() (size uintptr)
|
||||
|
||||
// Volume Management Functions
|
||||
//sys DefineDosDevice(flags uint32, deviceName *uint16, targetPath *uint16) (err error) = DefineDosDeviceW
|
||||
@@ -411,7 +421,7 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys VerQueryValue(block unsafe.Pointer, subBlock string, pointerToBufferPointer unsafe.Pointer, bufSize *uint32) (err error) = version.VerQueryValueW
|
||||
|
||||
// Process Status API (PSAPI)
|
||||
//sys EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
|
||||
//sys enumProcesses(processIds *uint32, nSize uint32, bytesReturned *uint32) (err error) = psapi.EnumProcesses
|
||||
//sys EnumProcessModules(process Handle, module *Handle, cb uint32, cbNeeded *uint32) (err error) = psapi.EnumProcessModules
|
||||
//sys EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *uint32, filterFlag uint32) (err error) = psapi.EnumProcessModulesEx
|
||||
//sys GetModuleInformation(process Handle, module Handle, modinfo *ModuleInfo, cb uint32) (err error) = psapi.GetModuleInformation
|
||||
@@ -439,6 +449,14 @@ func NewCallbackCDecl(fn interface{}) uintptr {
|
||||
//sys RtlAddFunctionTable(functionTable *RUNTIME_FUNCTION, entryCount uint32, baseAddress uintptr) (ret bool) = ntdll.RtlAddFunctionTable
|
||||
//sys RtlDeleteFunctionTable(functionTable *RUNTIME_FUNCTION) (ret bool) = ntdll.RtlDeleteFunctionTable
|
||||
|
||||
// Desktop Window Manager API (Dwmapi)
|
||||
//sys DwmGetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmGetWindowAttribute
|
||||
//sys DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) = dwmapi.DwmSetWindowAttribute
|
||||
|
||||
// Windows Multimedia API
|
||||
//sys TimeBeginPeriod (period uint32) (err error) [failretval != 0] = winmm.timeBeginPeriod
|
||||
//sys TimeEndPeriod (period uint32) (err error) [failretval != 0] = winmm.timeEndPeriod
|
||||
|
||||
// syscall interface implementation for other packages
|
||||
|
||||
// GetCurrentProcess returns the handle for the current process.
|
||||
@@ -748,7 +766,7 @@ func Utimes(path string, tv []Timeval) (err error) {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer Close(h)
|
||||
defer CloseHandle(h)
|
||||
a := NsecToFiletime(tv[0].Nanoseconds())
|
||||
w := NsecToFiletime(tv[1].Nanoseconds())
|
||||
return SetFileTime(h, nil, &a, &w)
|
||||
@@ -768,7 +786,7 @@ func UtimesNano(path string, ts []Timespec) (err error) {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
defer Close(h)
|
||||
defer CloseHandle(h)
|
||||
a := NsecToFiletime(TimespecToNsec(ts[0]))
|
||||
w := NsecToFiletime(TimespecToNsec(ts[1]))
|
||||
return SetFileTime(h, nil, &a, &w)
|
||||
@@ -826,6 +844,9 @@ const socket_error = uintptr(^uint32(0))
|
||||
//sys WSAStartup(verreq uint32, data *WSAData) (sockerr error) = ws2_32.WSAStartup
|
||||
//sys WSACleanup() (err error) [failretval==socket_error] = ws2_32.WSACleanup
|
||||
//sys WSAIoctl(s Handle, iocc uint32, inbuf *byte, cbif uint32, outbuf *byte, cbob uint32, cbbr *uint32, overlapped *Overlapped, completionRoutine uintptr) (err error) [failretval==socket_error] = ws2_32.WSAIoctl
|
||||
//sys WSALookupServiceBegin(querySet *WSAQUERYSET, flags uint32, handle *Handle) (err error) [failretval==socket_error] = ws2_32.WSALookupServiceBeginW
|
||||
//sys WSALookupServiceNext(handle Handle, flags uint32, size *int32, querySet *WSAQUERYSET) (err error) [failretval==socket_error] = ws2_32.WSALookupServiceNextW
|
||||
//sys WSALookupServiceEnd(handle Handle) (err error) [failretval==socket_error] = ws2_32.WSALookupServiceEnd
|
||||
//sys socket(af int32, typ int32, protocol int32) (handle Handle, err error) [failretval==InvalidHandle] = ws2_32.socket
|
||||
//sys sendto(s Handle, buf []byte, flags int32, to unsafe.Pointer, tolen int32) (err error) [failretval==socket_error] = ws2_32.sendto
|
||||
//sys recvfrom(s Handle, buf []byte, flags int32, from *RawSockaddrAny, fromlen *int32) (n int32, err error) [failretval==-1] = ws2_32.recvfrom
|
||||
@@ -963,7 +984,8 @@ func (sa *SockaddrUnix) sockaddr() (unsafe.Pointer, int32, error) {
|
||||
if n > 0 {
|
||||
sl += int32(n) + 1
|
||||
}
|
||||
if sa.raw.Path[0] == '@' {
|
||||
if sa.raw.Path[0] == '@' || (sa.raw.Path[0] == 0 && sl > 3) {
|
||||
// Check sl > 3 so we don't change unnamed socket behavior.
|
||||
sa.raw.Path[0] = 0
|
||||
// Don't count trailing NUL for abstract address.
|
||||
sl--
|
||||
@@ -1021,8 +1043,7 @@ func (rsa *RawSockaddrAny) Sockaddr() (Sockaddr, error) {
|
||||
for n < len(pp.Path) && pp.Path[n] != 0 {
|
||||
n++
|
||||
}
|
||||
bytes := (*[len(pp.Path)]byte)(unsafe.Pointer(&pp.Path[0]))[0:n]
|
||||
sa.Name = string(bytes)
|
||||
sa.Name = string(unsafe.Slice((*byte)(unsafe.Pointer(&pp.Path[0])), n))
|
||||
return sa, nil
|
||||
|
||||
case AF_INET:
|
||||
@@ -1108,9 +1129,13 @@ func Shutdown(fd Handle, how int) (err error) {
|
||||
}
|
||||
|
||||
func WSASendto(s Handle, bufs *WSABuf, bufcnt uint32, sent *uint32, flags uint32, to Sockaddr, overlapped *Overlapped, croutine *byte) (err error) {
|
||||
rsa, l, err := to.sockaddr()
|
||||
if err != nil {
|
||||
return err
|
||||
var rsa unsafe.Pointer
|
||||
var l int32
|
||||
if to != nil {
|
||||
rsa, l, err = to.sockaddr()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return WSASendTo(s, bufs, bufcnt, sent, flags, (*RawSockaddrAny)(unsafe.Pointer(rsa)), l, overlapped, croutine)
|
||||
}
|
||||
@@ -1350,6 +1375,17 @@ func SetsockoptIPv6Mreq(fd Handle, level, opt int, mreq *IPv6Mreq) (err error) {
|
||||
return syscall.EWINDOWS
|
||||
}
|
||||
|
||||
func EnumProcesses(processIds []uint32, bytesReturned *uint32) error {
|
||||
// EnumProcesses syscall expects the size parameter to be in bytes, but the code generated with mksyscall uses
|
||||
// the length of the processIds slice instead. Hence, this wrapper function is added to fix the discrepancy.
|
||||
var p *uint32
|
||||
if len(processIds) > 0 {
|
||||
p = &processIds[0]
|
||||
}
|
||||
size := uint32(len(processIds) * 4)
|
||||
return enumProcesses(p, size, bytesReturned)
|
||||
}
|
||||
|
||||
func Getpid() (pid int) { return int(GetCurrentProcessId()) }
|
||||
|
||||
func FindFirstFile(name *uint16, data *Win32finddata) (handle Handle, err error) {
|
||||
@@ -1609,6 +1645,11 @@ func SetConsoleCursorPosition(console Handle, position Coord) error {
|
||||
return setConsoleCursorPosition(console, *((*uint32)(unsafe.Pointer(&position))))
|
||||
}
|
||||
|
||||
func GetStartupInfo(startupInfo *StartupInfo) error {
|
||||
getStartupInfo(startupInfo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s NTStatus) Errno() syscall.Errno {
|
||||
return rtlNtStatusToDosErrorNoTeb(s)
|
||||
}
|
||||
@@ -1643,12 +1684,8 @@ func NewNTUnicodeString(s string) (*NTUnicodeString, error) {
|
||||
|
||||
// Slice returns a uint16 slice that aliases the data in the NTUnicodeString.
|
||||
func (s *NTUnicodeString) Slice() []uint16 {
|
||||
var slice []uint16
|
||||
hdr := (*unsafeheader.Slice)(unsafe.Pointer(&slice))
|
||||
hdr.Data = unsafe.Pointer(s.Buffer)
|
||||
hdr.Len = int(s.Length)
|
||||
hdr.Cap = int(s.MaximumLength)
|
||||
return slice
|
||||
slice := unsafe.Slice(s.Buffer, s.MaximumLength)
|
||||
return slice[:s.Length]
|
||||
}
|
||||
|
||||
func (s *NTUnicodeString) String() string {
|
||||
@@ -1671,12 +1708,8 @@ func NewNTString(s string) (*NTString, error) {
|
||||
|
||||
// Slice returns a byte slice that aliases the data in the NTString.
|
||||
func (s *NTString) Slice() []byte {
|
||||
var slice []byte
|
||||
hdr := (*unsafeheader.Slice)(unsafe.Pointer(&slice))
|
||||
hdr.Data = unsafe.Pointer(s.Buffer)
|
||||
hdr.Len = int(s.Length)
|
||||
hdr.Cap = int(s.MaximumLength)
|
||||
return slice
|
||||
slice := unsafe.Slice(s.Buffer, s.MaximumLength)
|
||||
return slice[:s.Length]
|
||||
}
|
||||
|
||||
func (s *NTString) String() string {
|
||||
@@ -1728,10 +1761,7 @@ func LoadResourceData(module, resInfo Handle) (data []byte, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h := (*unsafeheader.Slice)(unsafe.Pointer(&data))
|
||||
h.Data = unsafe.Pointer(ptr)
|
||||
h.Len = int(size)
|
||||
h.Cap = int(size)
|
||||
data = unsafe.Slice((*byte)(unsafe.Pointer(ptr)), size)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1802,3 +1832,87 @@ type PSAPI_WORKING_SET_EX_INFORMATION struct {
|
||||
// A PSAPI_WORKING_SET_EX_BLOCK union that indicates the attributes of the page at VirtualAddress.
|
||||
VirtualAttributes PSAPI_WORKING_SET_EX_BLOCK
|
||||
}
|
||||
|
||||
// CreatePseudoConsole creates a windows pseudo console.
|
||||
func CreatePseudoConsole(size Coord, in Handle, out Handle, flags uint32, pconsole *Handle) error {
|
||||
// We need this wrapper to manually cast Coord to uint32. The autogenerated wrappers only
|
||||
// accept arguments that can be casted to uintptr, and Coord can't.
|
||||
return createPseudoConsole(*((*uint32)(unsafe.Pointer(&size))), in, out, flags, pconsole)
|
||||
}
|
||||
|
||||
// ResizePseudoConsole resizes the internal buffers of the pseudo console to the width and height specified in `size`.
|
||||
func ResizePseudoConsole(pconsole Handle, size Coord) error {
|
||||
// We need this wrapper to manually cast Coord to uint32. The autogenerated wrappers only
|
||||
// accept arguments that can be casted to uintptr, and Coord can't.
|
||||
return resizePseudoConsole(pconsole, *((*uint32)(unsafe.Pointer(&size))))
|
||||
}
|
||||
|
||||
// DCB constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-dcb.
|
||||
const (
|
||||
CBR_110 = 110
|
||||
CBR_300 = 300
|
||||
CBR_600 = 600
|
||||
CBR_1200 = 1200
|
||||
CBR_2400 = 2400
|
||||
CBR_4800 = 4800
|
||||
CBR_9600 = 9600
|
||||
CBR_14400 = 14400
|
||||
CBR_19200 = 19200
|
||||
CBR_38400 = 38400
|
||||
CBR_57600 = 57600
|
||||
CBR_115200 = 115200
|
||||
CBR_128000 = 128000
|
||||
CBR_256000 = 256000
|
||||
|
||||
DTR_CONTROL_DISABLE = 0x00000000
|
||||
DTR_CONTROL_ENABLE = 0x00000010
|
||||
DTR_CONTROL_HANDSHAKE = 0x00000020
|
||||
|
||||
RTS_CONTROL_DISABLE = 0x00000000
|
||||
RTS_CONTROL_ENABLE = 0x00001000
|
||||
RTS_CONTROL_HANDSHAKE = 0x00002000
|
||||
RTS_CONTROL_TOGGLE = 0x00003000
|
||||
|
||||
NOPARITY = 0
|
||||
ODDPARITY = 1
|
||||
EVENPARITY = 2
|
||||
MARKPARITY = 3
|
||||
SPACEPARITY = 4
|
||||
|
||||
ONESTOPBIT = 0
|
||||
ONE5STOPBITS = 1
|
||||
TWOSTOPBITS = 2
|
||||
)
|
||||
|
||||
// EscapeCommFunction constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-escapecommfunction.
|
||||
const (
|
||||
SETXOFF = 1
|
||||
SETXON = 2
|
||||
SETRTS = 3
|
||||
CLRRTS = 4
|
||||
SETDTR = 5
|
||||
CLRDTR = 6
|
||||
SETBREAK = 8
|
||||
CLRBREAK = 9
|
||||
)
|
||||
|
||||
// PurgeComm constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-purgecomm.
|
||||
const (
|
||||
PURGE_TXABORT = 0x0001
|
||||
PURGE_RXABORT = 0x0002
|
||||
PURGE_TXCLEAR = 0x0004
|
||||
PURGE_RXCLEAR = 0x0008
|
||||
)
|
||||
|
||||
// SetCommMask constants. See https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setcommmask.
|
||||
const (
|
||||
EV_RXCHAR = 0x0001
|
||||
EV_RXFLAG = 0x0002
|
||||
EV_TXEMPTY = 0x0004
|
||||
EV_CTS = 0x0008
|
||||
EV_DSR = 0x0010
|
||||
EV_RLSD = 0x0020
|
||||
EV_BREAK = 0x0040
|
||||
EV_ERR = 0x0080
|
||||
EV_RING = 0x0100
|
||||
)
|
||||
|
||||
+195
-4
@@ -247,6 +247,7 @@ const (
|
||||
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007
|
||||
PROC_THREAD_ATTRIBUTE_UMS_THREAD = 0x00030006
|
||||
PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL = 0x0002000b
|
||||
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1093,7 +1094,33 @@ const (
|
||||
|
||||
SOMAXCONN = 0x7fffffff
|
||||
|
||||
TCP_NODELAY = 1
|
||||
TCP_NODELAY = 1
|
||||
TCP_EXPEDITED_1122 = 2
|
||||
TCP_KEEPALIVE = 3
|
||||
TCP_MAXSEG = 4
|
||||
TCP_MAXRT = 5
|
||||
TCP_STDURG = 6
|
||||
TCP_NOURG = 7
|
||||
TCP_ATMARK = 8
|
||||
TCP_NOSYNRETRIES = 9
|
||||
TCP_TIMESTAMPS = 10
|
||||
TCP_OFFLOAD_PREFERENCE = 11
|
||||
TCP_CONGESTION_ALGORITHM = 12
|
||||
TCP_DELAY_FIN_ACK = 13
|
||||
TCP_MAXRTMS = 14
|
||||
TCP_FASTOPEN = 15
|
||||
TCP_KEEPCNT = 16
|
||||
TCP_KEEPIDLE = TCP_KEEPALIVE
|
||||
TCP_KEEPINTVL = 17
|
||||
TCP_FAIL_CONNECT_ON_ICMP_ERROR = 18
|
||||
TCP_ICMP_ERROR_INFO = 19
|
||||
|
||||
UDP_NOCHECKSUM = 1
|
||||
UDP_SEND_MSG_SIZE = 2
|
||||
UDP_RECV_MAX_COALESCED_SIZE = 3
|
||||
UDP_CHECKSUM_COVERAGE = 20
|
||||
|
||||
UDP_COALESCED_INFO = 3
|
||||
|
||||
SHUT_RD = 0
|
||||
SHUT_WR = 1
|
||||
@@ -1243,6 +1270,51 @@ const (
|
||||
DnsSectionAdditional = 0x0003
|
||||
)
|
||||
|
||||
const (
|
||||
// flags of WSALookupService
|
||||
LUP_DEEP = 0x0001
|
||||
LUP_CONTAINERS = 0x0002
|
||||
LUP_NOCONTAINERS = 0x0004
|
||||
LUP_NEAREST = 0x0008
|
||||
LUP_RETURN_NAME = 0x0010
|
||||
LUP_RETURN_TYPE = 0x0020
|
||||
LUP_RETURN_VERSION = 0x0040
|
||||
LUP_RETURN_COMMENT = 0x0080
|
||||
LUP_RETURN_ADDR = 0x0100
|
||||
LUP_RETURN_BLOB = 0x0200
|
||||
LUP_RETURN_ALIASES = 0x0400
|
||||
LUP_RETURN_QUERY_STRING = 0x0800
|
||||
LUP_RETURN_ALL = 0x0FF0
|
||||
LUP_RES_SERVICE = 0x8000
|
||||
|
||||
LUP_FLUSHCACHE = 0x1000
|
||||
LUP_FLUSHPREVIOUS = 0x2000
|
||||
|
||||
LUP_NON_AUTHORITATIVE = 0x4000
|
||||
LUP_SECURE = 0x8000
|
||||
LUP_RETURN_PREFERRED_NAMES = 0x10000
|
||||
LUP_DNS_ONLY = 0x20000
|
||||
|
||||
LUP_ADDRCONFIG = 0x100000
|
||||
LUP_DUAL_ADDR = 0x200000
|
||||
LUP_FILESERVER = 0x400000
|
||||
LUP_DISABLE_IDN_ENCODING = 0x00800000
|
||||
LUP_API_ANSI = 0x01000000
|
||||
|
||||
LUP_RESOLUTION_HANDLE = 0x80000000
|
||||
)
|
||||
|
||||
const (
|
||||
// values of WSAQUERYSET's namespace
|
||||
NS_ALL = 0
|
||||
NS_DNS = 12
|
||||
NS_NLA = 15
|
||||
NS_BTH = 16
|
||||
NS_EMAIL = 37
|
||||
NS_PNRPNAME = 38
|
||||
NS_PNRPCLOUD = 39
|
||||
)
|
||||
|
||||
type DNSSRVData struct {
|
||||
Target *uint16
|
||||
Priority uint16
|
||||
@@ -2094,6 +2166,12 @@ const (
|
||||
ENABLE_LVB_GRID_WORLDWIDE = 0x10
|
||||
)
|
||||
|
||||
// Pseudo console related constants used for the flags parameter to
|
||||
// CreatePseudoConsole. See: https://learn.microsoft.com/en-us/windows/console/createpseudoconsole
|
||||
const (
|
||||
PSEUDOCONSOLE_INHERIT_CURSOR = 0x1
|
||||
)
|
||||
|
||||
type Coord struct {
|
||||
X int16
|
||||
Y int16
|
||||
@@ -2175,19 +2253,23 @@ type JOBOBJECT_BASIC_UI_RESTRICTIONS struct {
|
||||
}
|
||||
|
||||
const (
|
||||
// JobObjectInformationClass
|
||||
// JobObjectInformationClass for QueryInformationJobObject and SetInformationJobObject
|
||||
JobObjectAssociateCompletionPortInformation = 7
|
||||
JobObjectBasicAccountingInformation = 1
|
||||
JobObjectBasicAndIoAccountingInformation = 8
|
||||
JobObjectBasicLimitInformation = 2
|
||||
JobObjectBasicProcessIdList = 3
|
||||
JobObjectBasicUIRestrictions = 4
|
||||
JobObjectCpuRateControlInformation = 15
|
||||
JobObjectEndOfJobTimeInformation = 6
|
||||
JobObjectExtendedLimitInformation = 9
|
||||
JobObjectGroupInformation = 11
|
||||
JobObjectGroupInformationEx = 14
|
||||
JobObjectLimitViolationInformation2 = 35
|
||||
JobObjectLimitViolationInformation = 13
|
||||
JobObjectLimitViolationInformation2 = 34
|
||||
JobObjectNetRateControlInformation = 32
|
||||
JobObjectNotificationLimitInformation = 12
|
||||
JobObjectNotificationLimitInformation2 = 34
|
||||
JobObjectNotificationLimitInformation2 = 33
|
||||
JobObjectSecurityLimitInformation = 5
|
||||
)
|
||||
|
||||
@@ -3213,3 +3295,112 @@ type ModuleInfo struct {
|
||||
}
|
||||
|
||||
const ALL_PROCESSOR_GROUPS = 0xFFFF
|
||||
|
||||
type Rect struct {
|
||||
Left int32
|
||||
Top int32
|
||||
Right int32
|
||||
Bottom int32
|
||||
}
|
||||
|
||||
type GUIThreadInfo struct {
|
||||
Size uint32
|
||||
Flags uint32
|
||||
Active HWND
|
||||
Focus HWND
|
||||
Capture HWND
|
||||
MenuOwner HWND
|
||||
MoveSize HWND
|
||||
CaretHandle HWND
|
||||
CaretRect Rect
|
||||
}
|
||||
|
||||
const (
|
||||
DWMWA_NCRENDERING_ENABLED = 1
|
||||
DWMWA_NCRENDERING_POLICY = 2
|
||||
DWMWA_TRANSITIONS_FORCEDISABLED = 3
|
||||
DWMWA_ALLOW_NCPAINT = 4
|
||||
DWMWA_CAPTION_BUTTON_BOUNDS = 5
|
||||
DWMWA_NONCLIENT_RTL_LAYOUT = 6
|
||||
DWMWA_FORCE_ICONIC_REPRESENTATION = 7
|
||||
DWMWA_FLIP3D_POLICY = 8
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
||||
DWMWA_HAS_ICONIC_BITMAP = 10
|
||||
DWMWA_DISALLOW_PEEK = 11
|
||||
DWMWA_EXCLUDED_FROM_PEEK = 12
|
||||
DWMWA_CLOAK = 13
|
||||
DWMWA_CLOAKED = 14
|
||||
DWMWA_FREEZE_REPRESENTATION = 15
|
||||
DWMWA_PASSIVE_UPDATE_MODE = 16
|
||||
DWMWA_USE_HOSTBACKDROPBRUSH = 17
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
||||
DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
||||
DWMWA_BORDER_COLOR = 34
|
||||
DWMWA_CAPTION_COLOR = 35
|
||||
DWMWA_TEXT_COLOR = 36
|
||||
DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 37
|
||||
)
|
||||
|
||||
type WSAQUERYSET struct {
|
||||
Size uint32
|
||||
ServiceInstanceName *uint16
|
||||
ServiceClassId *GUID
|
||||
Version *WSAVersion
|
||||
Comment *uint16
|
||||
NameSpace uint32
|
||||
NSProviderId *GUID
|
||||
Context *uint16
|
||||
NumberOfProtocols uint32
|
||||
AfpProtocols *AFProtocols
|
||||
QueryString *uint16
|
||||
NumberOfCsAddrs uint32
|
||||
SaBuffer *CSAddrInfo
|
||||
OutputFlags uint32
|
||||
Blob *BLOB
|
||||
}
|
||||
|
||||
type WSAVersion struct {
|
||||
Version uint32
|
||||
EnumerationOfComparison int32
|
||||
}
|
||||
|
||||
type AFProtocols struct {
|
||||
AddressFamily int32
|
||||
Protocol int32
|
||||
}
|
||||
|
||||
type CSAddrInfo struct {
|
||||
LocalAddr SocketAddress
|
||||
RemoteAddr SocketAddress
|
||||
SocketType int32
|
||||
Protocol int32
|
||||
}
|
||||
|
||||
type BLOB struct {
|
||||
Size uint32
|
||||
BlobData *byte
|
||||
}
|
||||
|
||||
type ComStat struct {
|
||||
Flags uint32
|
||||
CBInQue uint32
|
||||
CBOutQue uint32
|
||||
}
|
||||
|
||||
type DCB struct {
|
||||
DCBlength uint32
|
||||
BaudRate uint32
|
||||
Flags uint32
|
||||
wReserved uint16
|
||||
XonLim uint16
|
||||
XoffLim uint16
|
||||
ByteSize uint8
|
||||
Parity uint8
|
||||
StopBits uint8
|
||||
XonChar byte
|
||||
XoffChar byte
|
||||
ErrorChar byte
|
||||
EofChar byte
|
||||
EvtChar byte
|
||||
wReserved1 uint16
|
||||
}
|
||||
|
||||
+325
-13
@@ -40,6 +40,7 @@ var (
|
||||
modadvapi32 = NewLazySystemDLL("advapi32.dll")
|
||||
modcrypt32 = NewLazySystemDLL("crypt32.dll")
|
||||
moddnsapi = NewLazySystemDLL("dnsapi.dll")
|
||||
moddwmapi = NewLazySystemDLL("dwmapi.dll")
|
||||
modiphlpapi = NewLazySystemDLL("iphlpapi.dll")
|
||||
modkernel32 = NewLazySystemDLL("kernel32.dll")
|
||||
modmswsock = NewLazySystemDLL("mswsock.dll")
|
||||
@@ -54,6 +55,7 @@ var (
|
||||
moduser32 = NewLazySystemDLL("user32.dll")
|
||||
moduserenv = NewLazySystemDLL("userenv.dll")
|
||||
modversion = NewLazySystemDLL("version.dll")
|
||||
modwinmm = NewLazySystemDLL("winmm.dll")
|
||||
modwintrust = NewLazySystemDLL("wintrust.dll")
|
||||
modws2_32 = NewLazySystemDLL("ws2_32.dll")
|
||||
modwtsapi32 = NewLazySystemDLL("wtsapi32.dll")
|
||||
@@ -85,6 +87,7 @@ var (
|
||||
procDeleteService = modadvapi32.NewProc("DeleteService")
|
||||
procDeregisterEventSource = modadvapi32.NewProc("DeregisterEventSource")
|
||||
procDuplicateTokenEx = modadvapi32.NewProc("DuplicateTokenEx")
|
||||
procEnumDependentServicesW = modadvapi32.NewProc("EnumDependentServicesW")
|
||||
procEnumServicesStatusExW = modadvapi32.NewProc("EnumServicesStatusExW")
|
||||
procEqualSid = modadvapi32.NewProc("EqualSid")
|
||||
procFreeSid = modadvapi32.NewProc("FreeSid")
|
||||
@@ -175,14 +178,20 @@ var (
|
||||
procDnsNameCompare_W = moddnsapi.NewProc("DnsNameCompare_W")
|
||||
procDnsQuery_W = moddnsapi.NewProc("DnsQuery_W")
|
||||
procDnsRecordListFree = moddnsapi.NewProc("DnsRecordListFree")
|
||||
procDwmGetWindowAttribute = moddwmapi.NewProc("DwmGetWindowAttribute")
|
||||
procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
|
||||
procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
|
||||
procGetAdaptersInfo = modiphlpapi.NewProc("GetAdaptersInfo")
|
||||
procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx")
|
||||
procGetIfEntry = modiphlpapi.NewProc("GetIfEntry")
|
||||
procAddDllDirectory = modkernel32.NewProc("AddDllDirectory")
|
||||
procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
|
||||
procCancelIo = modkernel32.NewProc("CancelIo")
|
||||
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
|
||||
procClearCommBreak = modkernel32.NewProc("ClearCommBreak")
|
||||
procClearCommError = modkernel32.NewProc("ClearCommError")
|
||||
procCloseHandle = modkernel32.NewProc("CloseHandle")
|
||||
procClosePseudoConsole = modkernel32.NewProc("ClosePseudoConsole")
|
||||
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
|
||||
procCreateDirectoryW = modkernel32.NewProc("CreateDirectoryW")
|
||||
procCreateEventExW = modkernel32.NewProc("CreateEventExW")
|
||||
@@ -197,6 +206,7 @@ var (
|
||||
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
|
||||
procCreatePipe = modkernel32.NewProc("CreatePipe")
|
||||
procCreateProcessW = modkernel32.NewProc("CreateProcessW")
|
||||
procCreatePseudoConsole = modkernel32.NewProc("CreatePseudoConsole")
|
||||
procCreateSymbolicLinkW = modkernel32.NewProc("CreateSymbolicLinkW")
|
||||
procCreateToolhelp32Snapshot = modkernel32.NewProc("CreateToolhelp32Snapshot")
|
||||
procDefineDosDeviceW = modkernel32.NewProc("DefineDosDeviceW")
|
||||
@@ -204,7 +214,9 @@ var (
|
||||
procDeleteProcThreadAttributeList = modkernel32.NewProc("DeleteProcThreadAttributeList")
|
||||
procDeleteVolumeMountPointW = modkernel32.NewProc("DeleteVolumeMountPointW")
|
||||
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
|
||||
procDisconnectNamedPipe = modkernel32.NewProc("DisconnectNamedPipe")
|
||||
procDuplicateHandle = modkernel32.NewProc("DuplicateHandle")
|
||||
procEscapeCommFunction = modkernel32.NewProc("EscapeCommFunction")
|
||||
procExitProcess = modkernel32.NewProc("ExitProcess")
|
||||
procExpandEnvironmentStringsW = modkernel32.NewProc("ExpandEnvironmentStringsW")
|
||||
procFindClose = modkernel32.NewProc("FindClose")
|
||||
@@ -228,6 +240,8 @@ var (
|
||||
procGenerateConsoleCtrlEvent = modkernel32.NewProc("GenerateConsoleCtrlEvent")
|
||||
procGetACP = modkernel32.NewProc("GetACP")
|
||||
procGetActiveProcessorCount = modkernel32.NewProc("GetActiveProcessorCount")
|
||||
procGetCommModemStatus = modkernel32.NewProc("GetCommModemStatus")
|
||||
procGetCommState = modkernel32.NewProc("GetCommState")
|
||||
procGetCommTimeouts = modkernel32.NewProc("GetCommTimeouts")
|
||||
procGetCommandLineW = modkernel32.NewProc("GetCommandLineW")
|
||||
procGetComputerNameExW = modkernel32.NewProc("GetComputerNameExW")
|
||||
@@ -246,9 +260,11 @@ var (
|
||||
procGetFileAttributesW = modkernel32.NewProc("GetFileAttributesW")
|
||||
procGetFileInformationByHandle = modkernel32.NewProc("GetFileInformationByHandle")
|
||||
procGetFileInformationByHandleEx = modkernel32.NewProc("GetFileInformationByHandleEx")
|
||||
procGetFileTime = modkernel32.NewProc("GetFileTime")
|
||||
procGetFileType = modkernel32.NewProc("GetFileType")
|
||||
procGetFinalPathNameByHandleW = modkernel32.NewProc("GetFinalPathNameByHandleW")
|
||||
procGetFullPathNameW = modkernel32.NewProc("GetFullPathNameW")
|
||||
procGetLargePageMinimum = modkernel32.NewProc("GetLargePageMinimum")
|
||||
procGetLastError = modkernel32.NewProc("GetLastError")
|
||||
procGetLogicalDriveStringsW = modkernel32.NewProc("GetLogicalDriveStringsW")
|
||||
procGetLogicalDrives = modkernel32.NewProc("GetLogicalDrives")
|
||||
@@ -312,6 +328,7 @@ var (
|
||||
procProcess32NextW = modkernel32.NewProc("Process32NextW")
|
||||
procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId")
|
||||
procPulseEvent = modkernel32.NewProc("PulseEvent")
|
||||
procPurgeComm = modkernel32.NewProc("PurgeComm")
|
||||
procQueryDosDeviceW = modkernel32.NewProc("QueryDosDeviceW")
|
||||
procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW")
|
||||
procQueryInformationJobObject = modkernel32.NewProc("QueryInformationJobObject")
|
||||
@@ -321,8 +338,13 @@ var (
|
||||
procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory")
|
||||
procReleaseMutex = modkernel32.NewProc("ReleaseMutex")
|
||||
procRemoveDirectoryW = modkernel32.NewProc("RemoveDirectoryW")
|
||||
procRemoveDllDirectory = modkernel32.NewProc("RemoveDllDirectory")
|
||||
procResetEvent = modkernel32.NewProc("ResetEvent")
|
||||
procResizePseudoConsole = modkernel32.NewProc("ResizePseudoConsole")
|
||||
procResumeThread = modkernel32.NewProc("ResumeThread")
|
||||
procSetCommBreak = modkernel32.NewProc("SetCommBreak")
|
||||
procSetCommMask = modkernel32.NewProc("SetCommMask")
|
||||
procSetCommState = modkernel32.NewProc("SetCommState")
|
||||
procSetCommTimeouts = modkernel32.NewProc("SetCommTimeouts")
|
||||
procSetConsoleCursorPosition = modkernel32.NewProc("SetConsoleCursorPosition")
|
||||
procSetConsoleMode = modkernel32.NewProc("SetConsoleMode")
|
||||
@@ -338,6 +360,7 @@ var (
|
||||
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
|
||||
procSetFilePointer = modkernel32.NewProc("SetFilePointer")
|
||||
procSetFileTime = modkernel32.NewProc("SetFileTime")
|
||||
procSetFileValidData = modkernel32.NewProc("SetFileValidData")
|
||||
procSetHandleInformation = modkernel32.NewProc("SetHandleInformation")
|
||||
procSetInformationJobObject = modkernel32.NewProc("SetInformationJobObject")
|
||||
procSetNamedPipeHandleState = modkernel32.NewProc("SetNamedPipeHandleState")
|
||||
@@ -348,6 +371,7 @@ var (
|
||||
procSetStdHandle = modkernel32.NewProc("SetStdHandle")
|
||||
procSetVolumeLabelW = modkernel32.NewProc("SetVolumeLabelW")
|
||||
procSetVolumeMountPointW = modkernel32.NewProc("SetVolumeMountPointW")
|
||||
procSetupComm = modkernel32.NewProc("SetupComm")
|
||||
procSizeofResource = modkernel32.NewProc("SizeofResource")
|
||||
procSleepEx = modkernel32.NewProc("SleepEx")
|
||||
procTerminateJobObject = modkernel32.NewProc("TerminateJobObject")
|
||||
@@ -366,6 +390,7 @@ var (
|
||||
procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx")
|
||||
procVirtualUnlock = modkernel32.NewProc("VirtualUnlock")
|
||||
procWTSGetActiveConsoleSessionId = modkernel32.NewProc("WTSGetActiveConsoleSessionId")
|
||||
procWaitCommEvent = modkernel32.NewProc("WaitCommEvent")
|
||||
procWaitForMultipleObjects = modkernel32.NewProc("WaitForMultipleObjects")
|
||||
procWaitForSingleObject = modkernel32.NewProc("WaitForSingleObject")
|
||||
procWriteConsoleW = modkernel32.NewProc("WriteConsoleW")
|
||||
@@ -444,9 +469,18 @@ var (
|
||||
procCommandLineToArgvW = modshell32.NewProc("CommandLineToArgvW")
|
||||
procSHGetKnownFolderPath = modshell32.NewProc("SHGetKnownFolderPath")
|
||||
procShellExecuteW = modshell32.NewProc("ShellExecuteW")
|
||||
procEnumChildWindows = moduser32.NewProc("EnumChildWindows")
|
||||
procEnumWindows = moduser32.NewProc("EnumWindows")
|
||||
procExitWindowsEx = moduser32.NewProc("ExitWindowsEx")
|
||||
procGetClassNameW = moduser32.NewProc("GetClassNameW")
|
||||
procGetDesktopWindow = moduser32.NewProc("GetDesktopWindow")
|
||||
procGetForegroundWindow = moduser32.NewProc("GetForegroundWindow")
|
||||
procGetGUIThreadInfo = moduser32.NewProc("GetGUIThreadInfo")
|
||||
procGetShellWindow = moduser32.NewProc("GetShellWindow")
|
||||
procGetWindowThreadProcessId = moduser32.NewProc("GetWindowThreadProcessId")
|
||||
procIsWindow = moduser32.NewProc("IsWindow")
|
||||
procIsWindowUnicode = moduser32.NewProc("IsWindowUnicode")
|
||||
procIsWindowVisible = moduser32.NewProc("IsWindowVisible")
|
||||
procMessageBoxW = moduser32.NewProc("MessageBoxW")
|
||||
procCreateEnvironmentBlock = moduserenv.NewProc("CreateEnvironmentBlock")
|
||||
procDestroyEnvironmentBlock = moduserenv.NewProc("DestroyEnvironmentBlock")
|
||||
@@ -454,6 +488,8 @@ var (
|
||||
procGetFileVersionInfoSizeW = modversion.NewProc("GetFileVersionInfoSizeW")
|
||||
procGetFileVersionInfoW = modversion.NewProc("GetFileVersionInfoW")
|
||||
procVerQueryValueW = modversion.NewProc("VerQueryValueW")
|
||||
proctimeBeginPeriod = modwinmm.NewProc("timeBeginPeriod")
|
||||
proctimeEndPeriod = modwinmm.NewProc("timeEndPeriod")
|
||||
procWinVerifyTrustEx = modwintrust.NewProc("WinVerifyTrustEx")
|
||||
procFreeAddrInfoW = modws2_32.NewProc("FreeAddrInfoW")
|
||||
procGetAddrInfoW = modws2_32.NewProc("GetAddrInfoW")
|
||||
@@ -461,6 +497,9 @@ var (
|
||||
procWSAEnumProtocolsW = modws2_32.NewProc("WSAEnumProtocolsW")
|
||||
procWSAGetOverlappedResult = modws2_32.NewProc("WSAGetOverlappedResult")
|
||||
procWSAIoctl = modws2_32.NewProc("WSAIoctl")
|
||||
procWSALookupServiceBeginW = modws2_32.NewProc("WSALookupServiceBeginW")
|
||||
procWSALookupServiceEnd = modws2_32.NewProc("WSALookupServiceEnd")
|
||||
procWSALookupServiceNextW = modws2_32.NewProc("WSALookupServiceNextW")
|
||||
procWSARecv = modws2_32.NewProc("WSARecv")
|
||||
procWSARecvFrom = modws2_32.NewProc("WSARecvFrom")
|
||||
procWSASend = modws2_32.NewProc("WSASend")
|
||||
@@ -718,6 +757,14 @@ func DuplicateTokenEx(existingToken Token, desiredAccess uint32, tokenAttributes
|
||||
return
|
||||
}
|
||||
|
||||
func EnumDependentServices(service Handle, activityState uint32, services *ENUM_SERVICE_STATUS, buffSize uint32, bytesNeeded *uint32, servicesReturned *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procEnumDependentServicesW.Addr(), 6, uintptr(service), uintptr(activityState), uintptr(unsafe.Pointer(services)), uintptr(buffSize), uintptr(unsafe.Pointer(bytesNeeded)), uintptr(unsafe.Pointer(servicesReturned)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func EnumServicesStatusEx(mgr Handle, infoLevel uint32, serviceType uint32, serviceState uint32, services *byte, bufSize uint32, bytesNeeded *uint32, servicesReturned *uint32, resumeHandle *uint32, groupName *uint16) (err error) {
|
||||
r1, _, e1 := syscall.Syscall12(procEnumServicesStatusExW.Addr(), 10, uintptr(mgr), uintptr(infoLevel), uintptr(serviceType), uintptr(serviceState), uintptr(unsafe.Pointer(services)), uintptr(bufSize), uintptr(unsafe.Pointer(bytesNeeded)), uintptr(unsafe.Pointer(servicesReturned)), uintptr(unsafe.Pointer(resumeHandle)), uintptr(unsafe.Pointer(groupName)), 0, 0)
|
||||
if r1 == 0 {
|
||||
@@ -1525,6 +1572,22 @@ func DnsRecordListFree(rl *DNSRecord, freetype uint32) {
|
||||
return
|
||||
}
|
||||
|
||||
func DwmGetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procDwmGetWindowAttribute.Addr(), 4, uintptr(hwnd), uintptr(attribute), uintptr(value), uintptr(size), 0, 0)
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DwmSetWindowAttribute(hwnd HWND, attribute uint32, value unsafe.Pointer, size uint32) (ret error) {
|
||||
r0, _, _ := syscall.Syscall6(procDwmSetWindowAttribute.Addr(), 4, uintptr(hwnd), uintptr(attribute), uintptr(value), uintptr(size), 0, 0)
|
||||
if r0 != 0 {
|
||||
ret = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetAdaptersAddresses(family uint32, flags uint32, reserved uintptr, adapterAddresses *IpAdapterAddresses, sizePointer *uint32) (errcode error) {
|
||||
r0, _, _ := syscall.Syscall6(procGetAdaptersAddresses.Addr(), 5, uintptr(family), uintptr(flags), uintptr(reserved), uintptr(unsafe.Pointer(adapterAddresses)), uintptr(unsafe.Pointer(sizePointer)), 0)
|
||||
if r0 != 0 {
|
||||
@@ -1557,6 +1620,15 @@ func GetIfEntry(pIfRow *MibIfRow) (errcode error) {
|
||||
return
|
||||
}
|
||||
|
||||
func AddDllDirectory(path *uint16) (cookie uintptr, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procAddDllDirectory.Addr(), 1, uintptr(unsafe.Pointer(path)), 0, 0)
|
||||
cookie = uintptr(r0)
|
||||
if cookie == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AssignProcessToJobObject(job Handle, process Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procAssignProcessToJobObject.Addr(), 2, uintptr(job), uintptr(process), 0)
|
||||
if r1 == 0 {
|
||||
@@ -1581,6 +1653,22 @@ func CancelIoEx(s Handle, o *Overlapped) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ClearCommBreak(handle Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procClearCommBreak.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ClearCommError(handle Handle, lpErrors *uint32, lpStat *ComStat) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procClearCommError.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(lpErrors)), uintptr(unsafe.Pointer(lpStat)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CloseHandle(handle Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCloseHandle.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if r1 == 0 {
|
||||
@@ -1589,6 +1677,11 @@ func CloseHandle(handle Handle) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ClosePseudoConsole(console Handle) {
|
||||
syscall.Syscall(procClosePseudoConsole.Addr(), 1, uintptr(console), 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
func ConnectNamedPipe(pipe Handle, overlapped *Overlapped) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procConnectNamedPipe.Addr(), 2, uintptr(pipe), uintptr(unsafe.Pointer(overlapped)), 0)
|
||||
if r1 == 0 {
|
||||
@@ -1718,6 +1811,14 @@ func CreateProcess(appName *uint16, commandLine *uint16, procSecurity *SecurityA
|
||||
return
|
||||
}
|
||||
|
||||
func createPseudoConsole(size uint32, in Handle, out Handle, flags uint32, pconsole *Handle) (hr error) {
|
||||
r0, _, _ := syscall.Syscall6(procCreatePseudoConsole.Addr(), 5, uintptr(size), uintptr(in), uintptr(out), uintptr(flags), uintptr(unsafe.Pointer(pconsole)), 0)
|
||||
if r0 != 0 {
|
||||
hr = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateSymbolicLink(symlinkfilename *uint16, targetfilename *uint16, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procCreateSymbolicLinkW.Addr(), 3, uintptr(unsafe.Pointer(symlinkfilename)), uintptr(unsafe.Pointer(targetfilename)), uintptr(flags))
|
||||
if r1&0xff == 0 {
|
||||
@@ -1772,6 +1873,14 @@ func DeviceIoControl(handle Handle, ioControlCode uint32, inBuffer *byte, inBuff
|
||||
return
|
||||
}
|
||||
|
||||
func DisconnectNamedPipe(pipe Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procDisconnectNamedPipe.Addr(), 1, uintptr(pipe), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetProcessHandle Handle, lpTargetHandle *Handle, dwDesiredAccess uint32, bInheritHandle bool, dwOptions uint32) (err error) {
|
||||
var _p0 uint32
|
||||
if bInheritHandle {
|
||||
@@ -1784,6 +1893,14 @@ func DuplicateHandle(hSourceProcessHandle Handle, hSourceHandle Handle, hTargetP
|
||||
return
|
||||
}
|
||||
|
||||
func EscapeCommFunction(handle Handle, dwFunc uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procEscapeCommFunction.Addr(), 2, uintptr(handle), uintptr(dwFunc), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ExitProcess(exitcode uint32) {
|
||||
syscall.Syscall(procExitProcess.Addr(), 1, uintptr(exitcode), 0, 0)
|
||||
return
|
||||
@@ -1985,6 +2102,22 @@ func GetActiveProcessorCount(groupNumber uint16) (ret uint32) {
|
||||
return
|
||||
}
|
||||
|
||||
func GetCommModemStatus(handle Handle, lpModemStat *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetCommModemStatus.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpModemStat)), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetCommState(handle Handle, lpDCB *DCB) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpDCB)), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0)
|
||||
if r1 == 0 {
|
||||
@@ -2125,6 +2258,14 @@ func GetFileInformationByHandleEx(handle Handle, class uint32, outBuffer *byte,
|
||||
return
|
||||
}
|
||||
|
||||
func GetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetime) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procGetFileTime.Addr(), 4, uintptr(handle), uintptr(unsafe.Pointer(ctime)), uintptr(unsafe.Pointer(atime)), uintptr(unsafe.Pointer(wtime)), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetFileType(filehandle Handle) (n uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetFileType.Addr(), 1, uintptr(filehandle), 0, 0)
|
||||
n = uint32(r0)
|
||||
@@ -2152,6 +2293,12 @@ func GetFullPathName(path *uint16, buflen uint32, buf *uint16, fname **uint16) (
|
||||
return
|
||||
}
|
||||
|
||||
func GetLargePageMinimum() (size uintptr) {
|
||||
r0, _, _ := syscall.Syscall(procGetLargePageMinimum.Addr(), 0, 0, 0, 0)
|
||||
size = uintptr(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func GetLastError() (lasterr error) {
|
||||
r0, _, _ := syscall.Syscall(procGetLastError.Addr(), 0, 0, 0, 0)
|
||||
if r0 != 0 {
|
||||
@@ -2320,11 +2467,8 @@ func GetShortPathName(longpath *uint16, shortpath *uint16, buflen uint32) (n uin
|
||||
return
|
||||
}
|
||||
|
||||
func GetStartupInfo(startupInfo *StartupInfo) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetStartupInfoW.Addr(), 1, uintptr(unsafe.Pointer(startupInfo)), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
func getStartupInfo(startupInfo *StartupInfo) {
|
||||
syscall.Syscall(procGetStartupInfoW.Addr(), 1, uintptr(unsafe.Pointer(startupInfo)), 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2726,6 +2870,14 @@ func PulseEvent(event Handle) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func PurgeComm(handle Handle, dwFlags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procPurgeComm.Addr(), 2, uintptr(handle), uintptr(dwFlags), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func QueryDosDevice(deviceName *uint16, targetPath *uint16, max uint32) (n uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procQueryDosDeviceW.Addr(), 3, uintptr(unsafe.Pointer(deviceName)), uintptr(unsafe.Pointer(targetPath)), uintptr(max))
|
||||
n = uint32(r0)
|
||||
@@ -2807,6 +2959,14 @@ func RemoveDirectory(path *uint16) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func RemoveDllDirectory(cookie uintptr) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procRemoveDllDirectory.Addr(), 1, uintptr(cookie), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ResetEvent(event Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procResetEvent.Addr(), 1, uintptr(event), 0, 0)
|
||||
if r1 == 0 {
|
||||
@@ -2815,6 +2975,14 @@ func ResetEvent(event Handle) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func resizePseudoConsole(pconsole Handle, size uint32) (hr error) {
|
||||
r0, _, _ := syscall.Syscall(procResizePseudoConsole.Addr(), 2, uintptr(pconsole), uintptr(size), 0)
|
||||
if r0 != 0 {
|
||||
hr = syscall.Errno(r0)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ResumeThread(thread Handle) (ret uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procResumeThread.Addr(), 1, uintptr(thread), 0, 0)
|
||||
ret = uint32(r0)
|
||||
@@ -2824,6 +2992,30 @@ func ResumeThread(thread Handle) (ret uint32, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func SetCommBreak(handle Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetCommBreak.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetCommMask(handle Handle, dwEvtMask uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetCommMask.Addr(), 2, uintptr(handle), uintptr(dwEvtMask), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetCommState(handle Handle, lpDCB *DCB) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetCommState.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(lpDCB)), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetCommTimeouts(handle Handle, timeouts *CommTimeouts) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetCommTimeouts.Addr(), 2, uintptr(handle), uintptr(unsafe.Pointer(timeouts)), 0)
|
||||
if r1 == 0 {
|
||||
@@ -2952,6 +3144,14 @@ func SetFileTime(handle Handle, ctime *Filetime, atime *Filetime, wtime *Filetim
|
||||
return
|
||||
}
|
||||
|
||||
func SetFileValidData(handle Handle, validDataLength int64) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetFileValidData.Addr(), 2, uintptr(handle), uintptr(validDataLength), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetHandleInformation(handle Handle, mask uint32, flags uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetHandleInformation.Addr(), 3, uintptr(handle), uintptr(mask), uintptr(flags))
|
||||
if r1 == 0 {
|
||||
@@ -3037,6 +3237,14 @@ func SetVolumeMountPoint(volumeMountPoint *uint16, volumeName *uint16) (err erro
|
||||
return
|
||||
}
|
||||
|
||||
func SetupComm(handle Handle, dwInQueue uint32, dwOutQueue uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procSetupComm.Addr(), 3, uintptr(handle), uintptr(dwInQueue), uintptr(dwOutQueue))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SizeofResource(module Handle, resInfo Handle) (size uint32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procSizeofResource.Addr(), 2, uintptr(module), uintptr(resInfo), 0)
|
||||
size = uint32(r0)
|
||||
@@ -3183,6 +3391,14 @@ func WTSGetActiveConsoleSessionId() (sessionID uint32) {
|
||||
return
|
||||
}
|
||||
|
||||
func WaitCommEvent(handle Handle, lpEvtMask *uint32, lpOverlapped *Overlapped) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWaitCommEvent.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(lpEvtMask)), uintptr(unsafe.Pointer(lpOverlapped)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func waitForMultipleObjects(count uint32, handles uintptr, waitAll bool, waitMilliseconds uint32) (event uint32, err error) {
|
||||
var _p0 uint32
|
||||
if waitAll {
|
||||
@@ -3469,12 +3685,8 @@ func EnumProcessModulesEx(process Handle, module *Handle, cb uint32, cbNeeded *u
|
||||
return
|
||||
}
|
||||
|
||||
func EnumProcesses(processIds []uint32, bytesReturned *uint32) (err error) {
|
||||
var _p0 *uint32
|
||||
if len(processIds) > 0 {
|
||||
_p0 = &processIds[0]
|
||||
}
|
||||
r1, _, e1 := syscall.Syscall(procEnumProcesses.Addr(), 3, uintptr(unsafe.Pointer(_p0)), uintptr(len(processIds)), uintptr(unsafe.Pointer(bytesReturned)))
|
||||
func enumProcesses(processIds *uint32, nSize uint32, bytesReturned *uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procEnumProcesses.Addr(), 3, uintptr(unsafe.Pointer(processIds)), uintptr(nSize), uintptr(unsafe.Pointer(bytesReturned)))
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -3777,9 +3989,9 @@ func setupUninstallOEMInf(infFileName *uint16, flags SUOI, reserved uintptr) (er
|
||||
return
|
||||
}
|
||||
|
||||
func CommandLineToArgv(cmd *uint16, argc *int32) (argv *[8192]*[8192]uint16, err error) {
|
||||
func commandLineToArgv(cmd *uint16, argc *int32) (argv **uint16, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procCommandLineToArgvW.Addr(), 2, uintptr(unsafe.Pointer(cmd)), uintptr(unsafe.Pointer(argc)), 0)
|
||||
argv = (*[8192]*[8192]uint16)(unsafe.Pointer(r0))
|
||||
argv = (**uint16)(unsafe.Pointer(r0))
|
||||
if argv == nil {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
@@ -3802,6 +4014,19 @@ func ShellExecute(hwnd Handle, verb *uint16, file *uint16, args *uint16, cwd *ui
|
||||
return
|
||||
}
|
||||
|
||||
func EnumChildWindows(hwnd HWND, enumFunc uintptr, param unsafe.Pointer) {
|
||||
syscall.Syscall(procEnumChildWindows.Addr(), 3, uintptr(hwnd), uintptr(enumFunc), uintptr(param))
|
||||
return
|
||||
}
|
||||
|
||||
func EnumWindows(enumFunc uintptr, param unsafe.Pointer) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procEnumWindows.Addr(), 2, uintptr(enumFunc), uintptr(param), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ExitWindowsEx(flags uint32, reason uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procExitWindowsEx.Addr(), 2, uintptr(flags), uintptr(reason), 0)
|
||||
if r1 == 0 {
|
||||
@@ -3810,6 +4035,35 @@ func ExitWindowsEx(flags uint32, reason uint32) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func GetClassName(hwnd HWND, className *uint16, maxCount int32) (copied int32, err error) {
|
||||
r0, _, e1 := syscall.Syscall(procGetClassNameW.Addr(), 3, uintptr(hwnd), uintptr(unsafe.Pointer(className)), uintptr(maxCount))
|
||||
copied = int32(r0)
|
||||
if copied == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetDesktopWindow() (hwnd HWND) {
|
||||
r0, _, _ := syscall.Syscall(procGetDesktopWindow.Addr(), 0, 0, 0, 0)
|
||||
hwnd = HWND(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func GetForegroundWindow() (hwnd HWND) {
|
||||
r0, _, _ := syscall.Syscall(procGetForegroundWindow.Addr(), 0, 0, 0, 0)
|
||||
hwnd = HWND(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func GetGUIThreadInfo(thread uint32, info *GUIThreadInfo) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procGetGUIThreadInfo.Addr(), 2, uintptr(thread), uintptr(unsafe.Pointer(info)), 0)
|
||||
if r1 == 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetShellWindow() (shellWindow HWND) {
|
||||
r0, _, _ := syscall.Syscall(procGetShellWindow.Addr(), 0, 0, 0, 0)
|
||||
shellWindow = HWND(r0)
|
||||
@@ -3825,6 +4079,24 @@ func GetWindowThreadProcessId(hwnd HWND, pid *uint32) (tid uint32, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func IsWindow(hwnd HWND) (isWindow bool) {
|
||||
r0, _, _ := syscall.Syscall(procIsWindow.Addr(), 1, uintptr(hwnd), 0, 0)
|
||||
isWindow = r0 != 0
|
||||
return
|
||||
}
|
||||
|
||||
func IsWindowUnicode(hwnd HWND) (isUnicode bool) {
|
||||
r0, _, _ := syscall.Syscall(procIsWindowUnicode.Addr(), 1, uintptr(hwnd), 0, 0)
|
||||
isUnicode = r0 != 0
|
||||
return
|
||||
}
|
||||
|
||||
func IsWindowVisible(hwnd HWND) (isVisible bool) {
|
||||
r0, _, _ := syscall.Syscall(procIsWindowVisible.Addr(), 1, uintptr(hwnd), 0, 0)
|
||||
isVisible = r0 != 0
|
||||
return
|
||||
}
|
||||
|
||||
func MessageBox(hwnd HWND, text *uint16, caption *uint16, boxtype uint32) (ret int32, err error) {
|
||||
r0, _, e1 := syscall.Syscall6(procMessageBoxW.Addr(), 4, uintptr(hwnd), uintptr(unsafe.Pointer(text)), uintptr(unsafe.Pointer(caption)), uintptr(boxtype), 0, 0)
|
||||
ret = int32(r0)
|
||||
@@ -3914,6 +4186,22 @@ func _VerQueryValue(block unsafe.Pointer, subBlock *uint16, pointerToBufferPoint
|
||||
return
|
||||
}
|
||||
|
||||
func TimeBeginPeriod(period uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(proctimeBeginPeriod.Addr(), 1, uintptr(period), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TimeEndPeriod(period uint32) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(proctimeEndPeriod.Addr(), 1, uintptr(period), 0, 0)
|
||||
if r1 != 0 {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WinVerifyTrustEx(hwnd HWND, actionId *GUID, data *WinTrustData) (ret error) {
|
||||
r0, _, _ := syscall.Syscall(procWinVerifyTrustEx.Addr(), 3, uintptr(hwnd), uintptr(unsafe.Pointer(actionId)), uintptr(unsafe.Pointer(data)))
|
||||
if r0 != 0 {
|
||||
@@ -3972,6 +4260,30 @@ func WSAIoctl(s Handle, iocc uint32, inbuf *byte, cbif uint32, outbuf *byte, cbo
|
||||
return
|
||||
}
|
||||
|
||||
func WSALookupServiceBegin(querySet *WSAQUERYSET, flags uint32, handle *Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWSALookupServiceBeginW.Addr(), 3, uintptr(unsafe.Pointer(querySet)), uintptr(flags), uintptr(unsafe.Pointer(handle)))
|
||||
if r1 == socket_error {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WSALookupServiceEnd(handle Handle) (err error) {
|
||||
r1, _, e1 := syscall.Syscall(procWSALookupServiceEnd.Addr(), 1, uintptr(handle), 0, 0)
|
||||
if r1 == socket_error {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WSALookupServiceNext(handle Handle, flags uint32, size *int32, querySet *WSAQUERYSET) (err error) {
|
||||
r1, _, e1 := syscall.Syscall6(procWSALookupServiceNextW.Addr(), 4, uintptr(handle), uintptr(flags), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(querySet)), 0, 0)
|
||||
if r1 == socket_error {
|
||||
err = errnoErr(e1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func WSARecv(s Handle, bufs *WSABuf, bufcnt uint32, recvd *uint32, flags *uint32, overlapped *Overlapped, croutine *byte) (err error) {
|
||||
r1, _, e1 := syscall.Syscall9(procWSARecv.Addr(), 7, uintptr(s), uintptr(unsafe.Pointer(bufs)), uintptr(bufcnt), uintptr(unsafe.Pointer(recvd)), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(overlapped)), uintptr(unsafe.Pointer(croutine)), 0, 0)
|
||||
if r1 == socket_error {
|
||||
|
||||
Vendored
+7
-6
@@ -1,8 +1,9 @@
|
||||
# github.com/danieljoos/wincred v1.1.2
|
||||
## explicit; go 1.13
|
||||
# github.com/danieljoos/wincred v1.2.3
|
||||
## explicit; go 1.18
|
||||
github.com/danieljoos/wincred
|
||||
# golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
|
||||
## explicit; go 1.17
|
||||
golang.org/x/sys/execabs
|
||||
golang.org/x/sys/internal/unsafeheader
|
||||
# github.com/keybase/go-keychain v0.0.1
|
||||
## explicit; go 1.21
|
||||
github.com/keybase/go-keychain
|
||||
# golang.org/x/sys v0.20.0
|
||||
## explicit; go 1.18
|
||||
golang.org/x/sys/windows
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -1,9 +1,10 @@
|
||||
//go:build windows
|
||||
|
||||
package wincred
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
winc "github.com/danieljoos/wincred"
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
@@ -52,9 +53,7 @@ func (h Wincred) Get(serverURL string) (string, string, error) {
|
||||
}
|
||||
|
||||
for _, attr := range g.Attributes {
|
||||
if strings.Compare(attr.Keyword, "label") == 0 &&
|
||||
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
|
||||
|
||||
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
|
||||
return g.UserName, string(g.CredentialBlob), nil
|
||||
}
|
||||
}
|
||||
@@ -73,11 +72,10 @@ func getTarget(serverURL string) (string, error) {
|
||||
}
|
||||
|
||||
var targets []string
|
||||
for i := range creds {
|
||||
attrs := creds[i].Attributes
|
||||
for _, attr := range attrs {
|
||||
for _, cred := range creds {
|
||||
for _, attr := range cred.Attributes {
|
||||
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
|
||||
targets = append(targets, creds[i].TargetName)
|
||||
targets = append(targets, cred.TargetName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,15 +109,15 @@ func exactMatch(serverURL, target url.URL) bool {
|
||||
}
|
||||
|
||||
func approximateMatch(serverURL, target url.URL) bool {
|
||||
//if scheme is missing assume it is the same as target
|
||||
// if scheme is missing assume it is the same as target
|
||||
if serverURL.Scheme == "" {
|
||||
serverURL.Scheme = target.Scheme
|
||||
}
|
||||
//if port is missing assume it is the same as target
|
||||
// if port is missing assume it is the same as target
|
||||
if serverURL.Port() == "" && target.Port() != "" {
|
||||
serverURL.Host = serverURL.Host + ":" + target.Port()
|
||||
}
|
||||
//if path is missing assume it is the same as target
|
||||
// if path is missing assume it is the same as target
|
||||
if serverURL.Path == "" {
|
||||
serverURL.Path = target.Path
|
||||
}
|
||||
@@ -134,16 +132,14 @@ func (h Wincred) List() (map[string]string, error) {
|
||||
}
|
||||
|
||||
resp := make(map[string]string)
|
||||
for i := range creds {
|
||||
attrs := creds[i].Attributes
|
||||
for _, attr := range attrs {
|
||||
if strings.Compare(attr.Keyword, "label") == 0 &&
|
||||
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
|
||||
|
||||
resp[creds[i].TargetName] = creds[i].UserName
|
||||
for _, cred := range creds {
|
||||
for _, attr := range cred.Attributes {
|
||||
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
|
||||
resp[cred.TargetName] = cred.UserName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
@@ -0,0 +1,291 @@
|
||||
//go:build windows
|
||||
|
||||
package wincred
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func TestWinCredHelper(t *testing.T) {
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||
Username: "foobar",
|
||||
Secret: "foobarbaz",
|
||||
}
|
||||
creds1 := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||
Username: "foobarbaz",
|
||||
Secret: "foobar",
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
|
||||
// check for and remove remaining credentials from previous fail tests
|
||||
oldauths, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for k, v := range oldauths {
|
||||
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if strings.Compare(k, creds1.ServerURL) == 0 && strings.Compare(v, creds1.Username) == 0 {
|
||||
if err := helper.Delete(creds1.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recount for credentials
|
||||
oldauths, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
username, secret, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if username != "foobar" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||
}
|
||||
|
||||
if secret != "foobarbaz" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||
}
|
||||
|
||||
auths, err := helper.List()
|
||||
if err != nil || len(auths)-len(oldauths) != 1 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
helper.Add(creds1)
|
||||
defer helper.Delete(creds1.ServerURL)
|
||||
newauths, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(newauths)-len(auths) != 1 {
|
||||
if err == nil {
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||
}
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||
}
|
||||
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
|
||||
// through variations on the URL
|
||||
func TestWinCredHelperRetrieveAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
{
|
||||
doc: "stored with port, retrieved without",
|
||||
storeURL: "https://foobar.docker.io:2376",
|
||||
readURL: "https://foobar.docker.io",
|
||||
},
|
||||
{
|
||||
doc: "stored as https, retrieved without scheme",
|
||||
storeURL: "https://foobar.docker.io",
|
||||
readURL: "foobar.docker.io",
|
||||
},
|
||||
{
|
||||
doc: "stored with path, retrieved without",
|
||||
storeURL: "https://foobar.docker.io/one/two",
|
||||
readURL: "https://foobar.docker.io",
|
||||
},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
c := &credentials.Credentials{ServerURL: tc.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL %q: %s", tc.storeURL, err)
|
||||
}
|
||||
if _, _, err := helper.Get(tc.readURL); err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q using %q", tc.storeURL, tc.readURL)
|
||||
}
|
||||
if err := helper.Delete(tc.storeURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperRetrieveStrict verifies that only matching secrets are
|
||||
// returned.
|
||||
func TestWinCredHelperRetrieveStrict(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
{
|
||||
doc: "stored as https, retrieved using http",
|
||||
storeURL: "https://foobar.docker.io:2376",
|
||||
readURL: "http://foobar.docker.io:2376",
|
||||
},
|
||||
{
|
||||
doc: "stored as http, retrieved using https",
|
||||
storeURL: "http://foobar.docker.io:2376",
|
||||
readURL: "https://foobar.docker.io:2376",
|
||||
},
|
||||
{
|
||||
// stored as http, retrieved without a scheme specified (hence, using the default https://)
|
||||
doc: "stored as http, retrieved without scheme",
|
||||
storeURL: "http://foobar.docker.io",
|
||||
readURL: "foobar.docker.io:5678",
|
||||
},
|
||||
{
|
||||
doc: "non-matching ports",
|
||||
storeURL: "https://foobar.docker.io:1234",
|
||||
readURL: "https://foobar.docker.io:5678",
|
||||
},
|
||||
// TODO: is this desired behavior? The other way round does work
|
||||
// {
|
||||
// doc: "non-matching ports (stored without port)",
|
||||
// storeURL: "https://foobar.docker.io",
|
||||
// readURL: "https://foobar.docker.io:5678",
|
||||
// },
|
||||
{
|
||||
doc: "non-matching paths",
|
||||
storeURL: "https://foobar.docker.io:1234/one/two",
|
||||
readURL: "https://foobar.docker.io:1234/five/six",
|
||||
},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.storeURL); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.storeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
c := &credentials.Credentials{ServerURL: tc.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL %q: %s", tc.storeURL, err)
|
||||
}
|
||||
if _, _, err := helper.Get(tc.readURL); err == nil {
|
||||
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", tc.storeURL, tc.readURL)
|
||||
}
|
||||
if err := helper.Delete(tc.storeURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperStoreRetrieve verifies that secrets stored in the
|
||||
// the keychain can be read back using the URL that was used to store them.
|
||||
func TestWinCredHelperStoreRetrieve(t *testing.T) {
|
||||
tests := []struct {
|
||||
url string
|
||||
}{
|
||||
{url: "foobar.docker.io"},
|
||||
{url: "foobar.docker.io:2376"},
|
||||
{url: "//foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376"},
|
||||
{url: "http://foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376/some/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
t.Cleanup(func() {
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.url); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("cleanup: failed to delete '%s': %v", tc.url, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Clean store before testing.
|
||||
for _, tc := range tests {
|
||||
if err := helper.Delete(tc.url); err != nil && !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Errorf("prepare: failed to delete '%s': %v", tc.url, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Note that we don't delete between individual tests here, to verify that
|
||||
// subsequent stores/overwrites don't affect storing / retrieving secrets.
|
||||
for i, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.url, func(t *testing.T) {
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: tc.url,
|
||||
Username: fmt.Sprintf("user-%d", i),
|
||||
Secret: fmt.Sprintf("secret-%d", i),
|
||||
}
|
||||
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Fatalf("Error: failed to store secret for URL: %s: %s", tc.url, err)
|
||||
}
|
||||
user, secret, err := helper.Get(tc.url)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: failed to read secret for URL %q: %s", tc.url, err)
|
||||
}
|
||||
if user != c.Username {
|
||||
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, tc.url)
|
||||
}
|
||||
if secret != c.Secret {
|
||||
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, tc.url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
helper := Wincred{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package wincred
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func TestWinCredHelper(t *testing.T) {
|
||||
creds := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v1",
|
||||
Username: "foobar",
|
||||
Secret: "foobarbaz",
|
||||
}
|
||||
creds1 := &credentials.Credentials{
|
||||
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||
Username: "foobarbaz",
|
||||
Secret: "foobar",
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
|
||||
// check for and remove remaining credentials from previous fail tests
|
||||
oldauths, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for k, v := range oldauths {
|
||||
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if strings.Compare(k, creds1.ServerURL) == 0 && strings.Compare(v, creds1.Username) == 0 {
|
||||
if err := helper.Delete(creds1.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recount for credentials
|
||||
oldauths, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := helper.Add(creds); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
username, secret, err := helper.Get(creds.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if username != "foobar" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobar", username)
|
||||
}
|
||||
|
||||
if secret != "foobarbaz" {
|
||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||
}
|
||||
|
||||
auths, err := helper.List()
|
||||
if err != nil || len(auths)-len(oldauths) != 1 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
helper.Add(creds1)
|
||||
defer helper.Delete(creds1.ServerURL)
|
||||
newauths, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(newauths)-len(auths) != 1 {
|
||||
if err == nil {
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||
}
|
||||
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||
}
|
||||
|
||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
|
||||
// through variations on the URL
|
||||
func TestWinCredHelperRetrieveAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
// stored with port, retrieved without
|
||||
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
|
||||
|
||||
// stored as https, retrieved without scheme
|
||||
{"https://foobar.docker.io", "foobar.docker.io"},
|
||||
|
||||
// stored with path, retrieved without
|
||||
{"https://foobar.docker.io/one/two", "https://foobar.docker.io"},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
|
||||
continue
|
||||
}
|
||||
if _, _, err := helper.Get(te.readURL); err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
|
||||
}
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperRetrieveStrict verifies that only matching secrets are
|
||||
// returned.
|
||||
func TestWinCredHelperRetrieveStrict(t *testing.T) {
|
||||
tests := []struct {
|
||||
storeURL string
|
||||
readURL string
|
||||
}{
|
||||
// stored as https, retrieved using http
|
||||
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
|
||||
|
||||
// stored as http, retrieved using https
|
||||
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
|
||||
|
||||
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
|
||||
{"http://foobar.docker.io", "foobar.docker.io:5678"},
|
||||
|
||||
// non-matching ports
|
||||
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
|
||||
|
||||
// non-matching ports TODO is this desired behavior? The other way round does work
|
||||
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
|
||||
|
||||
// non-matching paths
|
||||
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
|
||||
for _, te := range tests {
|
||||
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
|
||||
continue
|
||||
}
|
||||
if _, _, err := helper.Get(te.readURL); err == nil {
|
||||
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
|
||||
}
|
||||
helper.Delete(te.storeURL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWinCredHelperStoreRetrieve verifies that secrets stored in the
|
||||
// the keychain can be read back using the URL that was used to store them.
|
||||
func TestWinCredHelperStoreRetrieve(t *testing.T) {
|
||||
tests := []struct {
|
||||
url string
|
||||
}{
|
||||
{url: "foobar.docker.io"},
|
||||
{url: "foobar.docker.io:2376"},
|
||||
{url: "//foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376"},
|
||||
{url: "http://foobar.docker.io:2376"},
|
||||
{url: "https://foobar.docker.io:2376/some/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path"},
|
||||
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
|
||||
}
|
||||
|
||||
helper := Wincred{}
|
||||
defer func() {
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.url)
|
||||
}
|
||||
}()
|
||||
|
||||
// Clean store before testing.
|
||||
for _, te := range tests {
|
||||
helper.Delete(te.url)
|
||||
}
|
||||
|
||||
// Note that we don't delete between individual tests here, to verify that
|
||||
// subsequent stores/overwrites don't affect storing / retrieving secrets.
|
||||
for i, te := range tests {
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: te.url,
|
||||
Username: fmt.Sprintf("user-%d", i),
|
||||
Secret: fmt.Sprintf("secret-%d", i),
|
||||
}
|
||||
|
||||
if err := helper.Add(c); err != nil {
|
||||
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
|
||||
continue
|
||||
}
|
||||
user, secret, err := helper.Get(te.url)
|
||||
if err != nil {
|
||||
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
|
||||
continue
|
||||
}
|
||||
if user != c.Username {
|
||||
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
|
||||
}
|
||||
if secret != c.Secret {
|
||||
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
helper := Wincred{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user