File oauth2-clientd-0.10.0.obscpio of Package oauth2-clientd

07070100000000000081A40000000000000000000000016851E67900000038000000000000000000000000000000000000002100000000oauth2-clientd-0.10.0/.gitignore.mypy_cache
.*.swp
*.egg-info
build
dist
*~
__pycache__
07070100000001000081A40000000000000000000000016851E67900004755000000000000000000000000000000000000002000000000oauth2-clientd-0.10.0/.pylintrc[MASTER]

# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=

# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=

# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=

# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0

# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS

# Add files or directories matching the regex patterns to the ignore-list. The
# regex matches against paths.
ignore-paths=

# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=

# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=1

# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100

# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=

# Pickle collected data for later comparisons.
persistent=yes

# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes

# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no


[MESSAGES CONTROL]

# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=

# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=missing-module-docstring,
        missing-class-docstring,
        missing-function-docstring,
        line-too-long,
        raw-checker-failed,
        bad-inline-option,
        locally-disabled,
        file-ignored,
        suppressed-message,
        useless-suppression,
        deprecated-pragma,
        use-symbolic-message-instead,
        too-many-ancestors,
        too-many-instance-attributes,
        too-few-public-methods,
        too-many-branches,
        too-many-locals,
        too-many-statements,
        line-too-long,
        missing-class-docstring,
        missing-module-docstring,
        missing-function-docstring,
        too-many-instance-attributes,
        too-many-locals,
        logging-fstring-interpolation

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member


[REPORTS]

# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
# which contain the number of messages in each category, as well as 'statement'
# which is the total number of statements analyzed. This score is used by the
# global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)

# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=

# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text

# Tells whether to display a full report or only the messages.
reports=no

# Activate the evaluation score.
score=yes


[REFACTORING]

# Maximum number of nested blocks for function / method body
max-nested-blocks=5

# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error


[BASIC]

# Naming style matching correct argument names.
argument-naming-style=snake_case

# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=

# Naming style matching correct attribute names.
attr-naming-style=snake_case

# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=

# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
          bar,
          baz,
          toto,
          tutu,
          tata

# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=

# Naming style matching correct class attribute names.
class-attribute-naming-style=any

# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=

# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE

# Regular expression matching correct class constant names. Overrides class-
# const-naming-style.
#class-const-rgx=

# Naming style matching correct class names.
class-naming-style=PascalCase

# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=

# Naming style matching correct constant names.
const-naming-style=UPPER_CASE

# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=

# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1

# Naming style matching correct function names.
function-naming-style=snake_case

# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=

# Good variable names which should always be accepted, separated by a comma.
good-names=i,
           j,
           k,
           ex,
           Run,
           _

# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=

# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no

# Naming style matching correct inline iteration names.
inlinevar-naming-style=any

# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=

# Naming style matching correct method names.
method-naming-style=snake_case

# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=

# Naming style matching correct module names.
module-naming-style=snake_case

# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=

# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=

# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_

# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty

# Naming style matching correct variable names.
variable-naming-style=snake_case

# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=


[FORMAT]

# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=

# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$

# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4

# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
# tab).
indent-string='    '

# Maximum number of characters on a single line.
max-line-length=100

# Maximum number of lines in a module.
max-module-lines=1000

# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no

# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no


[LOGGING]

# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=new

# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
      XXX,
      TODO

# Regular expression of note tags to take in consideration.
#notes-rgx=


[SIMILARITIES]

# Comments are removed from the similarity computation
ignore-comments=yes

# Docstrings are removed from the similarity computation
ignore-docstrings=yes

# Imports are removed from the similarity computation
ignore-imports=no

# Signatures are removed from the similarity computation
ignore-signatures=no

# Minimum lines number of a similarity.
min-similarity-lines=4


[SPELLING]

# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4

# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=

# List of comma separated words that should be considered directives if they
# appear and the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:

# List of comma separated words that should not be checked.
spelling-ignore-words=

# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=

# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no


[STRING]

# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no

# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no


[TYPECHECK]

# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager

# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=

# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes

# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes

# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes

# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=

# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes

# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1

# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1

# List of decorators that change the signature of a decorated function.
signature-mutators=


[VARIABLES]

# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=

# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes

# List of names allowed to shadow builtins
allowed-redefined-builtins=

# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
          _cb

# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_

# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_

# Tells whether we should check for unused import in __init__ files.
init-import=no

# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io


[CLASSES]

# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no

# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
                      __new__,
                      setUp,
                      __post_init__

# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
                  _fields,
                  _replace,
                  _source,
                  _make

# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls

# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls


[DESIGN]

# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=

# Maximum number of arguments for function / method.
max-args=5

# Maximum number of attributes for a class (see R0902).
max-attributes=7

# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5

# Maximum number of branch for function / method body.
max-branches=12

# Maximum number of locals for function / method body.
max-locals=15

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of public methods for a class (see R0904).
max-public-methods=20

# Maximum number of return / yield for function / method body.
max-returns=6

# Maximum number of statements in function / method body.
max-statements=50

# Minimum number of public methods for a class (see R0903).
min-public-methods=2


[IMPORTS]

# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=

# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no

# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no

# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix

# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=

# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=

# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=

# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=

# Force import order to recognize a module as part of a third party library.
known-third-party=enchant

# Couples of modules and preferred modules, separated by a comma.
preferred-modules=


[EXCEPTIONS]

# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
                       Exception
07070100000002000081A40000000000000000000000016851E67900000806000000000000000000000000000000000000001F00000000oauth2-clientd-0.10.0/FILES.md# File Formats

## Access Token

The access token written to the output file or published via the UNIX socket is unmodified from the token received from the provider (i.e. unencrypted).  The format of the token itself is defined by the provider.  [RFC 6749](https://tools.ietf.org/html/rfc6749#page-10) describes the access token as "a string presenting an authorization issued to the client.  The string is usually opaque to the client."

## Session File

The session file is stored in JSON format using utf-8 encoding.  It consists of four top-level variables:
* `cryptoparams`
	* The cryptographic parameters required to access the information contained in the `data` field.
	* `algo` - The algorithm used to encrypt the data, currently only `"AES"` is supported.
	* `mode` - The cipher mode used to encrypt the data, currently only `"CTR"` is supported.
	* `key` - The key used to encrypt the data, itself encrypted using the RSA public key below then encoded as base64.
	* Different algorithms and modes will use different fields to describe their additional input. AES-CTR requires:
		* `nonce` - The nonce used to initialize the cipher encoded as base64.
* `data`- The session dictionary serialized as a JSON document, encrypted using the parameters above and encoded as base64.
	* `client`- The client information used when establishing the refresh token.
		* `client_id` - The client ID presented to the server
		* `client_secret` - The client secret presented to the server (optional)
	* `registration` - The registration dictionary used when establishing the refresh token. It contains the URIs and other fields required to contact the OAUTH2 server.
	* `tokendata` - The token dictionary provided by oauthlib. The fields match the fields defined in [RFC 6749](https://tools.ietf.org/html/rfc6749).
* `private_key` - The RSA private key used to decrypt the cryptographic key described in `cryptoparams` encoded as base64.
* `public_key` - The RSA public key used to encrypt the cryptographic key described in `cryptoparams` encoded as base64.
07070100000003000081A40000000000000000000000016851E67900004659000000000000000000000000000000000000001E00000000oauth2-clientd-0.10.0/LICENSE		    GNU GENERAL PUBLIC LICENSE
		       Version 2, June 1991

 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

			    Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it.  (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.)  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must show them these terms so they know their
rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  Finally, any free program is threatened constantly by software
patents.  We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary.  To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

  The precise terms and conditions for copying, distribution and
modification follow.

		    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License.  The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language.  (Hereinafter, translation is included without limitation in
the term "modification".)  Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope.  The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

  1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

  2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

    a) You must cause the modified files to carry prominent notices
    stating that you changed the files and the date of any change.

    b) You must cause any work that you distribute or publish, that in
    whole or in part contains or is derived from the Program or any
    part thereof, to be licensed as a whole at no charge to all third
    parties under the terms of this License.

    c) If the modified program normally reads commands interactively
    when run, you must cause it, when started running for such
    interactive use in the most ordinary way, to print or display an
    announcement including an appropriate copyright notice and a
    notice that there is no warranty (or else, saying that you provide
    a warranty) and that users may redistribute the program under
    these conditions, and telling the user how to view a copy of this
    License.  (Exception: if the Program itself is interactive but
    does not normally print such an announcement, your work based on
    the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole.  If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works.  But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

  3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

    a) Accompany it with the complete corresponding machine-readable
    source code, which must be distributed under the terms of Sections
    1 and 2 above on a medium customarily used for software interchange; or,

    b) Accompany it with a written offer, valid for at least three
    years, to give any third party, for a charge no more than your
    cost of physically performing source distribution, a complete
    machine-readable copy of the corresponding source code, to be
    distributed under the terms of Sections 1 and 2 above on a medium
    customarily used for software interchange; or,

    c) Accompany it with the information you received as to the offer
    to distribute corresponding source code.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form with such
    an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it.  For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable.  However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

  4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License.  Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

  5. You are not required to accept this License, since you have not
signed it.  However, nothing else grants you permission to modify or
distribute the Program or its derivative works.  These actions are
prohibited by law if you do not accept this License.  Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

  6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions.  You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

  7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all.  For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded.  In such case, this License incorporates
the limitation as if written in the body of this License.

  9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number.  If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

  10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

			    NO WARRANTY

  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

		     END OF TERMS AND CONDITIONS

	    How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) year name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary.  Here is a sample; alter the names:

  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
  `Gnomovision' (which makes passes at compilers) written by James Hacker.

  <signature of Ty Coon>, 1 April 1989
  Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs.  If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library.  If this is what you want to do, use the GNU Library General
Public License instead of this License.
07070100000004000081A40000000000000000000000016851E679000000CF000000000000000000000000000000000000001F00000000oauth2-clientd-0.10.0/Makefilebuild: oauth2_clientd/*.py scripts/oauth2-clientd
	python3 setup.py build

test: build
	mypy oauth2_clientd scripts/oauth2-clientd
	pylint oauth2_clientd
	pylint --from-stdin script < scripts/oauth2-clientd
07070100000005000081A40000000000000000000000016851E6790000211B000000000000000000000000000000000000002000000000oauth2-clientd-0.10.0/README.md# oauth2-clientd

oauth2-clientd is a script to manage OAUTH2 tokens for terminal or
headless Mail User Agents (MUAs) like fetchmail or mutt.

It can obtain a refresh token, securely store it, periodically update it,
and return access tokens on-demand via either a plaintext file or UNIX socket.

## Installation

You will need the following dependencies:

    $ pip3 install --user oauthlib requests-oauthlib python-daemon


Then to install:

    $ python3 setup.py install --user


## Getting started

To get started, you'll need to generate a refresh token.

    $ oauth2-clientd -a -c <clientid> [-P <provider>] /path/to/sessionfile


If the sessionfile exists, it will be not overwritten without --force.

If left unspecified, the default provider is Microsoft Office 365.

You'll be prompted for two things:
- The secret for the clientid, if any.
- The password for the private key (with confirmation)

Then you'll be presented with a URL to follow in your browser to authenticate
to your provider.

If the provider is configured to redirect to localhost, an HTTP listener
will be started on a random port.  Otherwise, you'll need to copy-and-paste
the authorization URL the provider sends as a response.  If your MUA is
running on a remote system, you can specify the port to use for the listener
with `-p <port>` to allow an SSH tunnel to be used to forward the response
to the script.

Once the refresh token is obtained, oauth2-clientd will wait until
interrupted, refreshing the refresh token 5 minutes before it expires.

## Output

By default, oauth2-clientd will refresh the token and output an access
token on stdout.  Your MUA will probably either use a command to fetch a
new access token or expect the access token to be written to a plaintext
file on disk.

oauth2-clientd exports tokens in two ways:

The following options can be used to start up an HTTP listener on the specified
UNIX socket.  If the socket exists it will be removed and replaced.

    $ oauth2-clientd -s /path/to/socket /path/to/sessionfile


Your MUA can be configured to retreive the access token with a simple command:

    $ curl --unix-socket /path/to/socket http://localhost


If your client requires a plaintext file, the following command will write out
such a file each time the token is refreshed:

    $ oauth2-clientd -f /path/to/file /path/to/sessionfile


Both options can be used at the same time but each can only be specified once.

## Background

The script can be daemonized using the -D option which takes the path
to a logfile as an argument.

    $ oauth2-clientd [other options] -D /path/to/logfile /path/to/sessionfile


## Security

The tokens are kept secure on disk using AES encryption.  Each time the
tokens are written, a new key is generated, and encrypted with the RSA
public key generated during the authorize step.  The password for the private
key is used during startup to unlock the saved tokens and is unused after
that.  The refresh token is kept in memory but never written to disk in
unencrypted form.  The access token is available in plaintext but should
be configured with a short expiration time.

## Configuration

With version 0.7, oauth2-clientd uses external configuration files to setup
provider definitions.  The defaults have remained the same and are shipped in
a package resource file named `builtin-providers.conf`.  This file,
`/etc/oauth2-client.conf`, and `~/.config/oauth2-clientd/oauth2-clientd.conf`
will be read in order and each is optional.  Additional configuration files
can be specified on the command line via the `-C <filename>`.  The defaults
can be cleared by using `-C ""` or `-C /dev/null` and subsequent uses of `-C`
will again be additive.

It is possible to extend provider definitions by inheriting from existing ones,
adding or clearing options as needed.  The most common use for this is
to specify site- or user- specific client IDs and optional client secrets
to existing profile definitions.  To clear an option, assign it an empty value.

### Format

The configuration file format is the familiar [.INI](https://en.wikipedia.org/wiki/INI_file)
file format used in many projects.  The `DEFAULT` section defines the default
provider definition using the `provider` option.  Other sections define
providers using the name of the section as the name of the provider.  The
options consumed by this tool are as follows:

- `authorize_endpoint`: The URI to use to obtain an authorization code.
- `token_endpoint`: The URI to use to obtain tokens using the authorization
code or the refresh token..
- `redirect_uri`: The URI to redirect the client to after successfully
obtaining the refresh token.  For this tool to work properly, it must be: `http://localhost`
- `sasl_method`: The SASL method used.  Will be `XOAUTH2` or `OAUTHBEARER`.
- `scope`: The scopes associated with the token.  These will be site and
resource specific.  Please reference the documentation for the service you
are using to obtain proper scopes for the resource being accessed.

For a full provider definition, reference the provided `builtin-providers.conf`
file.

### Example

The following example, using contrived credentials, extends the Office365
definition for your site and uses it as the default provider.  These
configuration sections can be added to a one of the known locations shown
above or in a new file used via `-C`.

    [DEFAULT]
    provider = site-o365
    
    [site-o365]
    inherits = office365
    client_id = e9b20c6d-641d-4cf4-878d-fe78ff79746f
    client_secret =

## Client IDs

A client ID and optional secret is required for oauth2-clientd to
successfully authenticate via OAUTH2.

### Office 365

For corporate Office 365, your administrator will have to add a client ID
to your organization's Active Directory instance.  Once a client ID and
optional client secret have been obtained, they can be specified using the
`-c <clientid>` option or by adding a `client_id= <clientid>` option to a
provider definition in a known configuration file.  See
[Configuration](#Configuration) above.

### Outlook.com personal accounts

Outlook.com personal accounts are not supported yet and need more research.

### Gmail and Google Workspace

For personal Gmail accounts, you'll need to register this application
as a valid application in the Google Cloud Platform [Console](https://console.cloud.google.com/apis/credentials).  The same applies to Workspace domains
but your account must have administrative privileges to do it.

If you haven't used this service prior to configuring these Client IDs, you'll
have to accept the terms of service and create a new project (or use an
existing one).

Once the project is created, select it and go to the APIs [pane](https://console.cloud.google.com/apis).

There, you'll need to configure the "OAuth Consent Screen" first.

You'll need to choose a user type. `External` will work for the purposes
of setting up the client IDs for this use.  Next you'll enter a workflow:

- (1) OAuth Consent Screen
  - App Information
    -  App name: `<oauth2-clientd>` -- This is for your own reference
    -  User support email: `<this will be a pull down of your registered email addresses or groups>`
    -  App logo: (optional)

  - App domain (optional)
  - Authorized domains (optional)
  - Developer contact information `<same email address as above>`

- (2) Scopes
  - Click on _Add or remove scopes_
  - Manually add scopes
    - `https://mail.google.com`

- (3) Test Users
  - Click on _Add users_
  - Add your gmail address


Next select the _Credentials_ pane.

Click _+ CREATE CREDENTIALS_ and select _OAuth Client ID_.  The
application type will be _Web Application._  Once you select it, a number
of other fields will appear.  The _Name_ field is for you to be able to
identify the client ID later and can be whatever you like.  Click
_+ Add URI_ under the _Authorized redirect URIs_ heading and add
`http://localhost` before clicking _Create_.  Once you've created it,
it will display the client ID and secret.  These will be available via
the Credentials console for you to reference in the future.

Once you have the client ID and secret, you can establish your tokens using

    $ oauth2-clientd -P google -c <clientid> -a /path/to/sessionfile

and then following the instructions under [Getting Started](#Getting-Started).
The client ID can also be specified in a new provider section in a known
configuration file.  See [Configuration](#Configuration) above.
07070100000006000041ED0000000000000000000000026851E67900000000000000000000000000000000000000000000002500000000oauth2-clientd-0.10.0/oauth2_clientd07070100000007000081A40000000000000000000000016851E67900000027000000000000000000000000000000000000003100000000oauth2-clientd-0.10.0/oauth2_clientd/__init__.py
__all__ = [ 'cli', 'sessionmanager' ]
07070100000008000081ED0000000000000000000000016851E67900004ED6000000000000000000000000000000000000002C00000000oauth2-clientd-0.10.0/oauth2_clientd/cli.pyimport os
import os.path
import argparse
import stat
import sys
import time
import signal
import shutil
import getpass
import contextlib
import subprocess
import logging
from configparser import ConfigParser
try:
    import importlib.resources as pkg_resources
except ImportError:
    import importlib_resources as pkg_resources # type: ignore

from typing import Any, Dict, List, Optional, TextIO, Union

import daemon # type: ignore
import daemon.pidfile # type: ignore
from lockfile import AlreadyLocked # type: ignore
from oauthlib.oauth2.rfc6749.errors import OAuth2Error # type: ignore

from .sessionmanager import OAuth2ClientManager
from .sessionmanager import NoTokenError, NoPrivateKeyError

log = logging.getLogger()
DATE_FORMAT='%Y-%m-%d %H:%M:%S'
LOG_FORMAT = "%(asctime)s.%(msecs)03d %(threadName)s[%(process)d] %(levelname)s %(message)s"

DEFAULT_CONFIG_PATHS = [
    '/etc/oauth2-clientd.conf',
    os.path.expanduser('~/.config/oauth2-clientd/oauth2-clientd.conf')
]

try:
    from contextlib import nullcontext
except ImportError:
    # pylint: disable=invalid-name
    class nullcontext(contextlib.AbstractContextManager): # type: ignore
        def __init__(self, enter_result=None):
            self.enter_result = enter_result

        def __enter__(self):
            return self.enter_result

        def __exit__(self, *excinfo):
            pass

def shutdown_listeners_and_exit(oaclient: OAuth2ClientManager) -> None:
    log.warning("Shutting down")
    oaclient.stop_file_writer()
    oaclient.stop_socket_listener()
    sys.exit(0)

class SignalHandler:
    def __init__(self, client: OAuth2ClientManager) -> None:
        self.client = client

    def __call__(self, signum: int, trace: Any) -> None:
        shutdown_listeners_and_exit(self.client)

def token_needs_refreshing(token: Dict[str, Any], threshold: int) -> bool:
    return token['expires_at'] + threshold > time.time()

# CLOCK_BOOTTIME was added in Python 3.7, so we'll use CLOCK_REALTIME on
# earlier releases.
def get_boottime() -> int:
    try:
        return int(time.clock_gettime(time.CLOCK_BOOTTIME))
    except AttributeError:
        return int(time.clock_gettime(time.CLOCK_REALTIME))

# This is a workaround. All implementations of sleep() in Python use
# CLOCK_MONOTONIC, which has the advantage of never going backward but it
# also stops while the system is suspended.  If the system is suspended for
# longer than the specified threshold, we'll miss the renewal window.
# This workaround uses the CLOCK_BOOTTIME clock, which does _not_
# stop while the system is suspended, but since there is no direct way to
# access clock_nanosleep directly from Python, we'll have to settle for
# a loop with short timeouts to check if we've passed the deadline.
# [There is the monotonic_time third-party module but it uses dlopen to
#  access clock_nanosleep and that's even worse of a hack IMO.]
def wallclock_sleep(timeout: int, step: int = 60) -> None:
    deadline = get_boottime() + timeout

    while timeout > 0:
        step = min(step, timeout)
        time.sleep(step)

        timeout = deadline - get_boottime()

def wait_for_refresh_timeout(oaclient: OAuth2ClientManager, thresh: int) -> None:
    if not oaclient.token:
        raise NoTokenError("No token to refresh")

    timeout = int(oaclient.access_token_expiry - thresh - time.time())

    if timeout > 0:
        log.info(f"Waiting {int(timeout)}s to refresh token.")
        wallclock_sleep(timeout)

    if time.time() > oaclient.access_token_expiry:
        log.info("Token has expired.")

def run_update_hook(update_hook: str, access_token: str) -> None:
    try:
        log.info(f"Running update hook {update_hook} for new access token.")
        subprocess.run([update_hook], input=access_token, text=True, timeout=5, check=True)
    except subprocess.TimeoutExpired as ex:
        log.warning(f"Update hook {update_hook} timed out after {ex.timeout}s.")
    except subprocess.CalledProcessError as ex:
        ret = ex.returncode
        if ret > 0:
            log.warning(f"Update hook {update_hook} failed.  Exited with status={ret}.")
        else:
            log.warning(f"Update hook {update_hook} terminated by signal {-ret}.")

def main_loop(oaclient: OAuth2ClientManager, sockname: Optional[str],
              filename: Optional[str], threshold: int = 300,
              update_hook: Optional[str] = None) -> None:

    if not oaclient.token:
        raise NoTokenError("No token to monitor")

    if token_needs_refreshing(oaclient.token, threshold):
        oaclient.refresh_token()

    oaclient.save_session()

    if sockname:
        oaclient.start_socket_listener(sockname)

    if filename:
        oaclient.start_file_writer(filename)

    # Assume the access token has been changed since the last startup
    if update_hook:
        run_update_hook(update_hook, oaclient.token['access_token'])

    try:
        while True:
            wait_for_refresh_timeout(oaclient, threshold)
            log.debug("Wait for refresh complete")
            oaclient.refresh_token()
            oaclient.save_session()

            if update_hook:
                run_update_hook(update_hook, oaclient.token['access_token'])
            elif log.level >= logging.DEBUG and not filename and not sockname:
                print("\nBEGIN ACCESS TOKEN")
                if oaclient.token and 'access_token' in oaclient.token:
                    print(oaclient.token['access_token'])
                else:
                    raise NoTokenError("Token was supposed to be refreshed but is missing")
    except KeyboardInterrupt:
        shutdown_listeners_and_exit(oaclient)

def parse_arguments(config: ConfigParser) -> argparse.Namespace:
    provider_help : Dict[str, Union[Optional[str], List[str]]]

    try:
        default_provider = config['DEFAULT']['provider']
        provider_help = {
            'default' : default_provider,
            'help' : f'provider to request tokens (default={default_provider})',
        }
    except KeyError:
        provider_help = {
            'default' : None,
            'help' : 'provider to request tokens',
        }

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('-h', '--help', action='store_true',
                        help='display this message and exit')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='increase verbosity')
    parser.add_argument('-d', '--debug', action='store_true',
                        help='enable debug output')
    parser.add_argument('-C', '--config', type=str, action='append',
                        help='specify config file -- can be invoked more than once; empty path or /dev/null will clear options loaded from previously read/default files')

    parser.add_argument('-a', '--authorize', action='store_true',
                        help='generate a new refresh token on startup using the default clientid and secret for the provider')
    parser.add_argument('-c', '--clientid', type=str, default=None,
                        help='specify client id to use for authorization if different than the default (or if there is no default).')
    parser.add_argument('-p', '--port', type=int, default=0,
                        help='specify port for http server (useful for tunneling to remote host)')
    parser.add_argument('-D', '--daemonize', type=str, dest='logfile',
                        help='detach and daemonize after user interaction is complete, logging to file')
    parser.add_argument('-f', '--file', type=str, default=None,
                        help='write access token to <file> (periodically, prior to expiration, if in daemon mode)')
    parser.add_argument('-i', '--pidfile', type=str, default=None, dest='pidfile',
                        help='write daemon pid to <pidfile>')
    parser.add_argument('-s', '--socket', type=str, default=None,
                        help='create a UNIX socket at <socket> with an http listener to provide access token on request')
    parser.add_argument('-P', '--provider', type=str, **provider_help) # type: ignore
    parser.add_argument('-l', '--list-providers', action='store_true',
                        help='print available providers and exit.')
    parser.add_argument('--dump-config', action='store_true',
                        help='display loaded configuration and exit')
    parser.add_argument('-q', '--quiet', action='store_true', help='limit unnecessary output')
    parser.add_argument('-t', '--threshold', type=int, default=300,
                        help='threshold before expiration to attempt to refresh tokens. (default=300s)')
    parser.add_argument('-u', '--update-hook', type=str, default=None,
                        help='path to command to call when token is updated (will receive access token on stdin)')
    parser.add_argument('--force', action='store_true', help='overwrite sessionfile if it exists')

    # Options that don't require a session file and shouldn't fail should the argument
    # be absent.
    (args, _) = parser.parse_known_args()
    if (args.list_providers or args.dump_config) and not args.help:
        return args

    parser.add_argument('sessionfile', help='path to store encrypted session and refresh token')

    if args.help:
        parser.print_help()
        sys.exit(0)

    args = parser.parse_args()
    return args

def resolve_registration(config: ConfigParser, provider: str, loops: Optional[List[str]] = None) -> Dict[str, str]:
    reg = dict(config[provider])
    if 'inherits' in reg:
        if loops is None:
            loops = [provider]
        elif provider in loops:
            raise FatalError(f"Config error: Provider '{provider}' is already in the dependency chain")
        else:
            loops.append(provider)
        if reg['inherits'] not in config:
            raise FatalError(f"Config error: Provider '{provider}' inherits from provider '{reg['inherits']}' which does not exist.")
        inherited = resolve_registration(config, reg['inherits'], loops)
        reg = { **inherited, **reg }

    return reg

_REQUIRED_KEYS = [ 'authorize_endpoint', 'token_endpoint',
                   'sasl_method', 'scope', 'redirect_uri' ]

def validate_registration(provider: str, registration: Dict[str, str]):
    for key in _REQUIRED_KEYS:
        if not key in registration:
            raise FatalError(f"Definition for provider '{provider}' is missing option '{key}'.")

class FatalError(RuntimeError):
    pass

def read_config(config: ConfigParser, path: str, verbose: bool) -> None:
    try:
        msg = f"Reading config from '{path}'"
        with open(path, encoding='utf8', errors="surrogateescape") as config_file:
            config.read_file(config_file)
            msg += "."
    except OSError as ex:
        msg += f" failed: {ex.strerror}. [ignoring]"
    finally:
        if verbose:
            print(msg, file=sys.stderr)

def dump_config(config: ConfigParser, stream: TextIO):
    print("Loaded configuration: ", file=stream)
    config.write(stream)

def dump_providers(config: ConfigParser, stream: TextIO):
    print(f"Available providers: {', '.join(config.sections())}", file=stream)

def update_logging(verbose: bool, debug: bool) -> None:
    loglevel = logging.WARNING
    if debug:
        loglevel = logging.DEBUG
    elif verbose:
        loglevel = logging.INFO

    log.setLevel(loglevel)

def early_verbose(args: List[str]) -> bool:
    return ('-v' in args or '--verbose' in args or
            '-d' in args or '--debug' in args)

def main() -> None:
    config = ConfigParser()

    stderr_log_handler = logging.StreamHandler(stream=sys.stderr)
    log.addHandler(stderr_log_handler)
    update_logging(False, False)

    # We read the configs prior to parsing the command line but we'll still want
    # to be able to report opening configs
    verbose = early_verbose(sys.argv)

    msg = "Reading 'builtin-providers.conf' from package data"
    try:
        text = pkg_resources.open_text('oauth2_clientd.data', 'builtin-providers.conf')
        config.read_file(text)
        if verbose:
            print(msg + ".", file=sys.stderr)
    except FileNotFoundError:
        print(msg + " failed.  Defaults may be unavailable.", file=sys.stderr)

    for path in DEFAULT_CONFIG_PATHS:
        read_config(config, path, verbose)

    args = parse_arguments(config)

    update_logging(args.verbose, args.debug)

    # If -C was used, we'll need to add those into the mix
    if args.config:
        for configfile in args.config:
            if configfile in ('', '/dev/null'):
                config = ConfigParser()
                if args.debug:
                    print("Resetting configuration.", file=sys.stderr)
                continue
            read_config(config, configfile, verbose)

    if args.dump_config:
        dump_config(config, sys.stdout)
        sys.exit(0)

    if args.list_providers:
        dump_providers(config, sys.stdout)
        sys.exit(0)

    if args.provider:
        provider = args.provider
    else:
        try:
            provider = config['DEFAULT']['provider']
        except KeyError as ex:
            raise FatalError('No default provider configured.') from ex

    if not provider in config:
        if args.debug:
            dump_config(config, sys.stderr)
        elif args.verbose:
            dump_providers(config, sys.stderr)

        raise FatalError(f"No provider '{provider}' configured.")

    if args.update_hook:
        try:
            hook_st = os.stat(args.update_hook)
        except IOError as ex:
            raise FatalError(f"Could not stat update hook {args.update_hook}: {ex.strerror}") from ex

        if not stat.S_ISREG(hook_st.st_mode):
            raise FatalError(f"Update hook {args.update_hook} is not a regular file.")

        if hook_st.st_mode & stat.S_IXUSR == 0:
            raise FatalError(f"Update hook {args.update_hook} is not executable.")

    if args.pidfile:
        pidfile_path = os.path.realpath(args.pidfile)
        oa2cd_pidfile = daemon.pidfile.TimeoutPIDLockFile(pidfile_path)
        # If we know the pidfile is there, we can skip asking the user
        # for the password and exit early.  This is racy and for convenience
        # only.  It's checked properly before we start the main loop.
        if oa2cd_pidfile.is_locked():
            pid = oa2cd_pidfile.read_pid()
            raise FatalError(f"PID file {pidfile_path} is already locked by PID {pid}.")
    else:
        oa2cd_pidfile = nullcontext()

    try:
        if args.authorize:
            registration = resolve_registration(config, provider)
            validate_registration(provider, registration)
            if args.clientid:
                clientid = args.clientid
            elif 'client_id' in registration:
                clientid = registration['client_id']
            else:
                raise FatalError(f"Provider {args.provider} has no default client id set.\nPlease provide one with --clientid.")

            client_data = {
                'client_id' : clientid,
            }

            if os.path.exists(args.sessionfile) and not args.force:
                raise FatalError(f"{args.sessionfile} already exists.")

            # A missing client_secret will cause a password prompt.
            # If the client_secret key is present, even with an empty
            # string or None, we'll use that.
            if 'client_secret' in registration and not args.clientid:
                client_data['client_secret'] = registration['client_secret']
            else:
                try:
                    secret = getpass.getpass(f"Secret for clientid {clientid} (leave empty if there is no secret): ")
                    if secret:
                        client_data['client_secret'] = secret
                except (EOFError, KeyboardInterrupt) as ex:
                    raise FatalError("\nFailed to obtain client secret.") from ex

            if 'tenant' in registration:
                client_data['tenant'] = registration['tenant']
            try:
                oaclient = OAuth2ClientManager.from_new_authorization(registration, client_data,
                                                               args.port)
            except OAuth2Error as ex:
                log.error(f"Failed to obtain authorization: {str(ex)}.")
                if args.debug:
                    raise ex from ex
                sys.exit(1)
            oaclient.save_session(args.sessionfile, overwrite=args.force)
        else:
            try:
                # NB: If we make a request before daemonizing, we'll have to
                # re-establish the session afterwards as the sockets backing
                # the connection pool will have been closed.
                oaclient = OAuth2ClientManager.from_saved_session(args.sessionfile)
                if not oaclient.token:
                    raise NoTokenError("Session didn't contain valid session.")
            except (FileNotFoundError, PermissionError) as ex:
                raise FatalError(f"Couldn't open session file: {str(ex)}") from ex
    except NoPrivateKeyError as ex:
        raise FatalError(f"\n{str(ex)}") from ex

    daemonize = False
    if args.logfile:
        daemonize = True

    if not args.file and not args.socket:
        columns = shutil.get_terminal_size((80, 25))[0]
        if not args.quiet:
            raise FatalError("No file or socket was specified")

        if (oaclient.token and
                token_needs_refreshing(oaclient.token, args.threshold)):
            oaclient.refresh_token()

        if oaclient.token and 'access_token' in oaclient.token:
            if not args.quiet:
                raise FatalError("No file, socket, or update hook was specified")
            if oaclient.token and 'access_token' in oaclient.token:
                if not args.quiet:
                    print("Current access token follows:", file=sys.stderr)
                    print(columns * '-', file=sys.stderr)
                print(oaclient.token['access_token'])
                if not args.quiet:
                    print(columns * '-', file=sys.stderr)
            else:
                log.error("No valid access token found.")
            sys.exit(0)
        else:
            daemonize = False

    logfile: Optional[TextIO] = None
    if daemonize:
        try:
            # We open it here just to get an error for the user before
            # we daemonize.
            # pylint: disable=consider-using-with
            logfile = open(args.logfile, 'w+', encoding='utf8', errors="surrogateescape")
            logfile_handler = logging.StreamHandler(stream=logfile)
            log.addHandler(logfile_handler)
        except (OSError, IOError) as ex:
            raise FatalError(f"Failed to open logfile {logfile}: {ex.args[1]}.") from ex

        context = daemon.DaemonContext(files_preserve=[logfile],
                                       working_directory=os.getcwd(),
                                       pidfile=oa2cd_pidfile,
                                       stdout=sys.stdout,
                                       stderr=sys.stderr)
        context.signal_map = {
            signal.SIGTERM: SignalHandler,
            signal.SIGHUP: 'terminate',
        }
    else:
        context = oa2cd_pidfile

    try:
        with context:
            if logfile:
                sys.stderr.close()
                sys.stdout.close()
                sys.stderr = logfile
                sys.stdout = logfile
                log.removeHandler(stderr_log_handler)
            else:
                logfile_handler = stderr_log_handler

            # Now that we're starting up for real, some timestamps and other
            # information may be useful for logging.
            logfile_formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=DATE_FORMAT)
            logfile_handler.setFormatter(logfile_formatter)

            main_loop(oaclient, args.socket, args.file, args.threshold, args.update_hook)
    except AlreadyLocked as ex:
        raise FatalError(f"{ex} by PID {oa2cd_pidfile.read_pid()}") from ex
07070100000009000041ED0000000000000000000000026851E67900000000000000000000000000000000000000000000002A00000000oauth2-clientd-0.10.0/oauth2_clientd/data0707010000000A000081A40000000000000000000000016851E67900000000000000000000000000000000000000000000003600000000oauth2-clientd-0.10.0/oauth2_clientd/data/__init__.py0707010000000B000081A40000000000000000000000016851E679000002A9000000000000000000000000000000000000004100000000oauth2-clientd-0.10.0/oauth2_clientd/data/builtin-providers.conf[DEFAULT]
provider = office365

[google]
authorize_endpoint = https://accounts.google.com/o/oauth2/auth
token_endpoint = https://accounts.google.com/o/oauth2/token
redirect_uri = http://localhost
sasl_method = OAUTHBEARER
scope = https://mail.google.com/

[office365]
authorize_endpoint = https://login.microsoftonline.com/common/oauth2/v2.0/authorize
token_endpoint = https://login.microsoftonline.com/common/oauth2/v2.0/token
redirect_uri = http://localhost
sasl_method = XOAUTH2
scope = offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send

# Alias
[microsoft]
inherits = office365
0707010000000C000081A40000000000000000000000016851E67900001206000000000000000000000000000000000000003300000000oauth2-clientd-0.10.0/oauth2_clientd/encryption.pyimport getpass
import os
import sys

from typing import Dict, Tuple

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

from .helpers import b64decode, b64encode_str

class NoPrivateKeyError(RuntimeError):
    """Cannot unlock private key"""

def generate_rsa_private_key() -> rsa.RSAPrivateKeyWithSerialization:
    return rsa.generate_private_key(public_exponent=65537, key_size=2048,
                                    backend=default_backend())

def encrypt_private_key(private_key: rsa.RSAPrivateKeyWithSerialization,
                        password_bytes: bytes) -> bytes:

    encryption = serialization.BestAvailableEncryption(password_bytes)
    return private_key.private_bytes(encoding=serialization.Encoding.PEM,
                                     format=serialization.PrivateFormat.PKCS8,
                                     encryption_algorithm=encryption)

def get_password(minlen: int = 10) -> bytes:
    password_bytes = None
    while password_bytes is None:
        try:
            pw1 = getpass.getpass(f"Enter password for new private key (min {minlen} chars): ")
            if len(pw1) < 10:
                print(f"Password too short.  Must be longer than {minlen} characters.",
                      file=sys.stderr)
                continue
            pw2 = getpass.getpass("Repeat: ")
        except (KeyboardInterrupt, EOFError) as ex:
            raise NoPrivateKeyError("Cannot create private key without password.") from ex

        if pw1 == pw2:
            password_bytes = bytes(pw1.encode())
        else:
            print("Passwords don't match.  Try again.")

    return password_bytes

def serialize_public_key(public_key: rsa.RSAPublicKeyWithSerialization) -> bytes:
    return public_key.public_bytes(encoding=serialization.Encoding.PEM,
                                   format=serialization.PublicFormat.SubjectPublicKeyInfo)

def generate_rsa_keypair() -> Dict[str, bytes]:
    private_key = generate_rsa_private_key()
    password = get_password()
    private_key_pem = encrypt_private_key(private_key, password)
    public_key_pem = serialize_public_key(private_key.public_key())

    keypair = {
        'public_key_pem' : public_key_pem,
        'private_key_pem' : private_key_pem,
        }

    del private_key

    return keypair

def crypto_padding() -> padding.OAEP:
    return padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                        algorithm=hashes.SHA256(), label=None)

def encrypt(public_key_pem: bytes, data: bytes) -> Tuple[bytes, Dict[str, str]]:
    public_key = serialization.load_pem_public_key(public_key_pem, backend=default_backend())

    if not isinstance(public_key, rsa.RSAPublicKey):
        raise TypeError("Expected RSA key, got {type(public_key)}")

    key = os.urandom(32)
    nonce = os.urandom(16)

    params: Dict[str, str] = {
        'algo' : 'AES',
        'mode' : 'CTR',
        'key' : b64encode_str(public_key.encrypt(key, crypto_padding())),
        'nonce' : b64encode_str(nonce),
    }

    cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=default_backend())
    encryptor = cipher.encryptor()
    encrypted_data = encryptor.update(data) + encryptor.finalize()

    return encrypted_data, params


def decrypt(private_key: rsa.RSAPrivateKeyWithSerialization, cryptoparams: Dict[str, str],
            data: bytes) -> bytes:

    if cryptoparams['algo'] != 'AES':
        raise NotImplementedError(f"Support for cryptographic algorithm {cryptoparams['algo']} not implemented.")

    if cryptoparams['mode'] != 'CTR':
        raise NotImplementedError(f"Support for cryptographic mode {cryptoparams['mode']} not implemented.")

    key = private_key.decrypt(b64decode(cryptoparams['key']), crypto_padding())
    del private_key

    nonce = b64decode(cryptoparams['nonce'])
    cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=default_backend())
    decryptor = cipher.decryptor()
    return decryptor.update(data) + decryptor.finalize()

def decrypt_private_key(private_key_pem: bytes,
                        password_bytes: bytes) -> rsa.RSAPrivateKey:
    ret = serialization.load_pem_private_key(private_key_pem, password=password_bytes,
                                             backend=default_backend())
    if not isinstance(ret, rsa.RSAPrivateKey):
        raise TypeError(f"Expected RSAPrivateKey not {type(ret)}")

    return ret
0707010000000D000081A40000000000000000000000016851E679000003A0000000000000000000000000000000000000003000000000oauth2-clientd-0.10.0/oauth2_clientd/helpers.pyimport base64
import json

from typing import Any, Dict, Union

def encode_dict(dict_to_dump: Dict[Any, Any]) -> bytes:
    dump = bytes(json.dumps(dict_to_dump), 'utf-8')
    return base64.urlsafe_b64encode(dump)

def decode_dict(json_dict: bytes) -> str:
    text = base64.urlsafe_b64decode(json_dict)
    return json.loads(text)

def b64encode(data: Union[bytes, str]) -> bytes:
    if not isinstance(data, bytes):
        data = bytes(data, 'utf-8')

    return base64.urlsafe_b64encode(data)

def b64encode_str(data: bytes) -> str:
    return b64encode(data).decode('utf-8')

def b64decode(data: Union[bytes, str]) -> bytes:
    if not isinstance(data, bytes):
        data = bytes(data, 'utf-8')

    return base64.urlsafe_b64decode(data)

def b64decode_str(data: Union[bytes, str]) -> str:
    if not isinstance(data, bytes):
        data = bytes(data, 'utf-8')

    return base64.urlsafe_b64encode(data).decode('utf-8')
0707010000000E000081A40000000000000000000000016851E679000044EE000000000000000000000000000000000000003700000000oauth2-clientd-0.10.0/oauth2_clientd/sessionmanager.pyimport os
import os.path
import threading
import http.server
import socket
import select
import sys
import urllib.parse
import json
import secrets
import hashlib
import base64
import getpass
import stat
import logging

from typing import cast, Any, Dict, Optional, Tuple, Type, Union

from atomicwrites import atomic_write

from requests_oauthlib import OAuth2Session # type: ignore

from .encryption import generate_rsa_keypair, decrypt_private_key, encrypt, decrypt
from .encryption import NoPrivateKeyError
from .helpers import b64decode, b64encode_str, encode_dict

log = logging.getLogger(__name__)

class NoTokenError(RuntimeError):
    """The token is not available"""

try:
    from http.server import ThreadingHTTPServer
except ImportError:
    from socketserver import ThreadingMixIn
    class ThreadingHTTPServer(ThreadingMixIn, http.server.HTTPServer): # type: ignore
        daemon_threads = True

class _RedirectURIHandler(http.server.BaseHTTPRequestHandler):
    def log_request(self, code: Union[int, str] = '-',
                    size: Union[int, str] = '-') -> None:
        if log.level >= logging.DEBUG:
            super().log_request(code, size)

    def do_HEAD(self) -> None:
        # pylint: disable=invalid-name
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def _write_already_provided(self) -> None:
        self.wfile.write(b'The authorization redirect has already been provided ' +
                         b'and this server will shut down shortly.')

    def _write_redirect_completed(self) -> None:
        self.wfile.write(b'Authorization redirect completed. You may '
                         b'close this window.')

    def _write_invalid_request(self) -> None:
        self.wfile.write(b'The requested URI does not represent an authorization redirect.')

    # pylint: disable=invalid-name
    def do_GET(self) -> None:
        self.do_HEAD()
        self.wfile.write(b'<html><head><title>Authorizaton result</title></head>')
        self.wfile.write(b'<body><p>')

        path = 'http://localhost' + self.path
        server = cast(_ThreadingHTTPServerWithContext, self.server)
        if server.context.validate_authurl(path):
            with server.context.authurl_lock:
                if server.context.authurl:
                    self._write_already_provided()
                else:
                    server.context.authurl = path
                    self._write_redirect_completed()
        else:
            self._write_invalid_request()
        self.wfile.write(b'</p></body></html>')

class _TokenSocketHandler(http.server.BaseHTTPRequestHandler):
    def log_request(self, code: Union[int, str] = '-',
                    size: Union[int, str] = '-') -> None:
        if log.level >= logging.INFO:
            super().log_request(code, size)

    # pylint: disable=invalid-name
    def do_HEAD(self) -> None:
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()

    # pylint: disable=invalid-name
    def do_GET(self) -> None:
        self.do_HEAD()
        server = cast(_ThreadingHTTPServerWithContext, self.server)
        with server.context.token_lock:
            if (not server.context.token or
                    not 'access_token' in server.context.token):
                raise NoTokenError("Cannot retreive access token")
            response = server.context.token['access_token']

        self.wfile.write(bytes(response, 'utf-8'))

class _ThreadingHTTPServerWithContext(ThreadingHTTPServer):
    def __init__(self, address: Tuple[str, int],
                 handler: Type[http.server.BaseHTTPRequestHandler],
                 context: 'OAuth2ClientManager') -> None:
        super().__init__(address, handler)
        self.context = context

class _UnixSocketThreadingHTTPServer(_ThreadingHTTPServerWithContext):
    address_family = socket.AF_UNIX
    def __init__(self, filename: str,
                 handler: Type[http.server.BaseHTTPRequestHandler],
                 context: 'OAuth2ClientManager') -> None:
        super().__init__((filename, 0), handler, context)

    def server_bind(self) -> None:
        self.socket.bind(self.server_address[0])

    def get_request(self) -> Tuple[Any, Tuple[str, int]]:
        req, _ = super().get_request()
        return req, self.server_address

class OAuth2ClientManager:
    def __init__(self, registration: Dict[str, str], client: Dict[str, str]) -> None:
        self._registration = registration
        self.client = client
        self.session_file_path: Optional[str] = None
        self.public_key_pem: Optional[bytes] = None
        self.saved_session: Dict[str, Any] = {}
        self.session: OAuth2Session = None

        self.token: Optional[Dict[str, Any]] = None
        self.token_lock = threading.Lock()
        self.token_changed = threading.Condition(self.token_lock)

        self.authurl: Optional[str] = None
        self.authurl_lock = threading.Lock()

        self._server: Optional[_ThreadingHTTPServerWithContext] = None
        self._server_thread: Optional[threading.Thread] = None

        self._file_thread: Optional[threading.Thread] = None
        self._file_thread_exit = threading.Event()

    @property
    def access_token_expiry(self) -> float:
        if not self.token:
            raise NoTokenError("No valid token found.")
        if 'expires_at' not in self.token:
            raise ValueError("Token is missing expiration")
        with self.token_lock:
            expiry = self.token['expires_at']
        return expiry

    @classmethod
    def from_saved_session(cls, path: str) -> 'OAuth2ClientManager':
        with open(path, 'rb') as session_file:
            saved_session = json.loads(session_file.read())

        private_key_pem_bytes = bytes(saved_session['private_key'], 'utf-8')
        public_key_pem_bytes = bytes(saved_session['public_key'], 'utf-8')

        private_key = None
        while private_key is None:
            try:
                password = getpass.getpass("Enter password for private key: ")
            except (KeyboardInterrupt, EOFError) as ex:
                raise NoPrivateKeyError("Cannot unlock private key without password.") from ex

            if not password:
                continue

            password_bytes = bytes(password.encode())
            try:
                private_key = decrypt_private_key(private_key_pem_bytes, password_bytes)
            except ValueError as ex: # Usually bad password
                print(ex)

        data = decrypt(private_key, saved_session['cryptoparams'],
                       b64decode(saved_session['data']))

        del private_key

        session = json.loads(b64decode(data))

        obj = cls(session['registration'], session['client'])
        obj.session_file_path = path
        obj.saved_session = saved_session
        obj.public_key_pem = public_key_pem_bytes

        obj.token = session['tokendata']
        obj.session = OAuth2Session(session['client'], token=obj.token)

        return obj

    def save_session(self, path: Optional[str] = None, overwrite: bool = True) -> None:
        if path is None and self.session_file_path:
            path = self.session_file_path
        elif self.session_file_path is None and path:
            self.session_file_path = path
        else:
            raise ValueError("No path specified and no default available.")

        if self.public_key_pem is None:
            raise ValueError("No public key available for encryption.")

        data_dict = {
            'client' : self.client,
            'registration' : self._registration,
            'tokendata' : self.token,
        }

        data, params = encrypt(self.public_key_pem, encode_dict(data_dict))
        self.saved_session['data'] = b64encode_str(data)
        self.saved_session['cryptoparams'] = params

        if path is None:
            raise RuntimeError("No session file named for write.")

        jsondata = json.dumps(self.saved_session, sort_keys=True, indent=4)
        del self.saved_session['data']
        del self.saved_session['cryptoparams']
        with atomic_write(path, overwrite=overwrite) as session_file:
            print(jsondata, file=session_file)

    def _start_server(self) -> None:
        if self._server_thread:
            raise RuntimeError("Server thread already running.")
        if not self._server:
            raise RuntimeError("HTTP server not set up yet.")

        self._server.context = self
        self._server_thread = threading.Thread(target=self._server.serve_forever, name='SocketThread')
        self._server_thread.start()

    def _setup_redirect_listener(self, port: int = 0) -> None:
        self._server = _ThreadingHTTPServerWithContext(('127.0.0.1', port), _RedirectURIHandler, self)

    def _get_redirect_listener_port(self) -> int:
        if not self._server:
            raise RuntimeError("No server configured.")
        return self._server.server_address[1]

    def _stop_server(self) -> None:
        if self._server:
            log.debug("Telling HTTP server to shutdown")
            self._server.shutdown()
            self._server = None

        if self._server_thread:
            log.debug("Waiting for HTTP server to shutdown")
            self._server_thread.join()
            self._server_thread = None
            log.debug("HTTP server has shutdown")

    @staticmethod
    def _generate_pkce_context() -> Tuple[str, Dict[str, str]]:
        verifier = secrets.token_urlsafe(90)
        digest = hashlib.sha256(verifier.encode()).digest()
        challenge = base64.urlsafe_b64encode(digest)[:-1].decode('utf-8')

        pkce_challenge = {
            'code_challenge_method' : 'S256',
            'code_challenge' : challenge,
        }
        return (verifier, pkce_challenge)

    @staticmethod
    def _print_authurl_prompt() -> None:
        print('Please enter the full callback URL: ', end='', flush=True)

    def _inform_user_of_listener(self) -> None:
        if not self._server:
            return

        port = self._get_redirect_listener_port()
        listening = f"\nA listener has been started at localhost:{port}.  When you follow the link, the authorization response will be received automatically."
        listening += f"\nIf using this system remotely, you may wish to forward the port to this host by creating a new SSH session with the following options: '-L {port}:localhost:{port}' prior to following the link.\n"
        print(listening)

    @staticmethod
    def validate_authurl(url: str) -> bool:
        """Validate that a url could potentially be an authurl by testing for the 'code' query variable"""
        querystring = urllib.parse.urlparse(url).query
        qvars = urllib.parse.parse_qs(querystring)
        return 'code' in qvars

    # This handles racing with the http listener
    def _wait_for_authurl_on_stdin(self) -> None:
        self._print_authurl_prompt()
        while True:
            (readers, _, _) = select.select([sys.stdin], [], [], 0.5)
            with self.authurl_lock:
                if not self.authurl and readers:
                    try:
                        url = sys.stdin.readline()
                        if self.validate_authurl(url):
                            self.authurl = url
                        else:
                            print("Error: No authcode provided.")
                            self._print_authurl_prompt()
                            continue
                    except KeyboardInterrupt:
                        break
                elif self.authurl:
                    print("(not necessary any longer)\nResponse provided by browser session.\n")
                if self.authurl:
                    break


    @classmethod
    def from_new_authorization(cls, registration: Dict[str, str], client: Dict[str, str],
                               port: int = 0) -> 'OAuth2ClientManager':
        obj = cls(registration, client)
        keypair = generate_rsa_keypair()
        obj.saved_session = {
                'public_key' : keypair['public_key_pem'].decode('utf-8'),
                'private_key' : keypair['private_key_pem'].decode('utf-8'),
                }
        obj.public_key_pem = keypair['public_key_pem']
        obj._new_authorization(port)

        return obj

    def _new_authorization(self, port: int = 0) -> None:
        redirect_uri = self._registration['redirect_uri']
        if 'http://localhost' in redirect_uri:
            self._setup_redirect_listener(port)
            port = self._get_redirect_listener_port()

            redirect_uri = f'http://localhost:{port}'

        self.session = OAuth2Session(self.client['client_id'], redirect_uri=redirect_uri,
                                     scope=self._registration['scope'])

        verifier, pkce_challenge = self._generate_pkce_context()


        authorization_url, _ = self.session.authorization_url(self._registration['authorize_endpoint'],
                                                              **pkce_challenge)

        print(f'Please go to {authorization_url} and authorize access.')
        if self._server:
            self._start_server()
            self._inform_user_of_listener()
        self._wait_for_authurl_on_stdin()
        if self._server:
            self._stop_server()

        if not self.authurl:
            raise RuntimeError("Stopped waiting for authurl but none found.")

        # oauthlib expects redirects to be https -- no need for localhost
        if 'http://localhost' in self.authurl:
            os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

        # o365 requires the 'offline_access' scope in the request to issue
        # refresh tokens but strips it in the response. oauthlib views that
        # as a changed scope event that is handled as an error unless relaxed.
        os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'
        self.token = self.session.fetch_token(self._registration['token_endpoint'],
                                              authorization_response=self.authurl,
                                              include_client_id=True,
                                              **self.client, code_verifier=verifier)

    def refresh_token(self) -> None:
        log.info("Starting token refresh")
        new_token = self.session.refresh_token(self._registration['token_endpoint'], **self.client)
        log.info("Token refreshed")

        with self.token_changed:
            self.token = new_token
            self.token_changed.notify()

    def get_access_token(self) -> str:
        if not self.token or 'access_token' not in self.token:
            raise NoTokenError("No access token available")
        with self.token_lock:
            access_token = self.token['access_token']
        return access_token

    def write_access_token(self, filename: str) -> None:
        if not self.token or 'access_token' not in self.token:
            raise NoTokenError("No access token available.")
        try:
            with atomic_write(filename, overwrite=True) as access_file:
                print(self.token['access_token'], file=access_file)
        except OSError as ex:
            log.warning(f"Failed to write access token to file '{filename}': {ex.strerror}")

    def _file_writer(self, filename: str) -> None:
        if not self.token or 'access_token' not in self.token:
            raise NoTokenError("No access token available.")
        my_token: Optional[str] = None
        while True:
            needs_write = False
            with self.token_changed:
                if my_token == self.token['access_token']:
                    self.token_changed.wait()
                else:
                    my_token = self.token['access_token']
                    needs_write = True

            if not my_token:
                raise NoTokenError("Access token changed but is unavailable.")

            if needs_write:
                log.info(f"Writing out new access token to {filename}")
                self.write_access_token(filename)
            if self._file_thread_exit.is_set():
                log.debug("_file_writer: Exiting")
                break

    def start_file_writer(self, filename: str) -> None:
        self._file_thread = threading.Thread(target=self._file_writer, args=((filename),), name='FileWriterThread')
        self._file_thread.start()

    def stop_file_writer(self) -> None:
        if self._file_thread:
            log.debug("Telling file thread to exit")
            self._file_thread_exit.set()
            with self.token_changed:
                self.token_changed.notify()
            log.debug("Waiting for file thread to exit")
            self._file_thread.join()
            log.debug("File thread has exited")

    def start_socket_listener(self, filename: str) -> None:
        if self._server or self._server_thread:
            raise RuntimeError("Server already running")

        log.debug(f"Starting HTTP listener on {filename}")

        if os.path.exists(filename):
            sock_stat = os.stat(filename)
            if stat.S_ISSOCK(sock_stat.st_mode):
                os.unlink(filename)
            else:
                raise OSError("{filename} already exists but is not a socket.  Will not replace.")
        self._server = _UnixSocketThreadingHTTPServer(filename, _TokenSocketHandler, self)
        self._server.context = self
        os.chmod(filename, 0o600)

        self._start_server()

    def stop_socket_listener(self) -> None:
        self._stop_server()
0707010000000F000081A40000000000000000000000016851E679000001FE000000000000000000000000000000000000002500000000oauth2-clientd-0.10.0/pyproject.toml[project]
name = "oauth2-clientd"
version = "0.10.0"
description = "OAUTH2 client that caches refresh tokens securely"
authors = [
    {name = "Jeff Mahoney",email = "jeffm@suse.com"}
]
license = {text = "GPL-2.0"}
readme = "README.md"
requires-python = ">=3.7"
dependencies = [
    "requests-oauthlib",
    "python-daemon",
    "cryptography",
    "atomicwrites"
]

[project.scripts]
oauth2-clientd = 'oauth2_clientd.cli:main'

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
07070100000010000041ED0000000000000000000000026851E67900000000000000000000000000000000000000000000001E00000000oauth2-clientd-0.10.0/scripts07070100000011000081ED0000000000000000000000016851E679000000ED000000000000000000000000000000000000002D00000000oauth2-clientd-0.10.0/scripts/oauth2-clientd#!/usr/bin/python3

import sys
from oauth2_clientd import cli

if __name__ == '__main__':
    try:
        cli.main()
    except (cli.FatalError, OSError) as ex:
        print(str(ex) + "  Exiting.", file=sys.stderr)
        sys.exit(1)
07070100000012000081ED0000000000000000000000016851E67900000076000000000000000000000000000000000000001F00000000oauth2-clientd-0.10.0/setup.py#!/usr/bin/python3
# vim: sw=4 ts=4 et si:
"""
Setup file for installation
"""

from setuptools import setup

setup()
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!186 blocks
openSUSE Build Service is sponsored by