runff 1.0 commit

This commit is contained in:
rock64
2019-04-29 16:09:00 +02:00
commit f1567f989b
1268 changed files with 98652 additions and 0 deletions

118
CONTRIBUTING.md Executable file
View File

@@ -0,0 +1,118 @@
# Contribution guidelines
**SimpleSAMLphp welcomes all contributions**. It is impossible to make a product like this without the efforts of many people, so please don't be shy and share your help with us. Even the tiniest contribution can make a difference!
These guidelines briefly explain how to contribute to SimpleSAMLphp effectively and consistently, making sure to keep high quality standards and making it easier for you to contribute.
<!-- {{TOC}} -->
## Team members
Currently, the core team members are:
* Jaime Pérez Crespo, *main developer and release manager*, UNINETT <jaime.perez@uninett.no>
* Olav Morken, *main developer*, UNINETT <olav.morken@uninett.no>
* Andreas Åkre Solberg, *architect and original developer*, UNINETT <andreas.solberg@uninett.no>
* Hanne Moa, *developer*, UNINETT <hanne.moa@uninett.no>
We have been lucky enough to have so many people help us through the years. SimpleSAMLphp wouldn't have reached so far without them. We want to thank them from here, but unfortunately they are so many it is nearly impossible to mention all of them. [Here is a Github page that summarizes everyone's contributions](https://github.com/simplesamlphp/simplesamlphp/graphs/contributors?from=2007-09-09&to=2015-09-06&type=c).
***Big thanks to you all!***
## First things first
Before embarking yourself in a contribution, please make sure you are familiar with the way SimpleSAMLphp is written, the way it works, and what is required or not.
* Make sure to read [the documentation](https://simplesamlphp.org/docs/stable/). If you use the search engine in the website, please verify that you are reading the latest stable version. If you want to make a change, check [the development branch of the documentation](https://simplesamlphp.org/docs/development/).
* If you have a question about **using SimpleSAMLphp**, please use [the mailing list](http://groups.google.com/group/simplesamlphp).
* If you have a question about **developing SimpleSAMLphp**, please ask in the [development mailing list](http://groups.google.com/group/simplesamlphp-dev).
* If you think you have discovered a bug, please check the [issue tracker](https://github.com/simplesamlphp/simplesamlphp/issues) and the [pull requests](https://github.com/simplesamlphp/simplesamlphp/pulls) to verify it hasn't been reported already.
## Contributing code
New features are always welcome, provided they will be useful to someone apart from yourself. Please take a look at the [list of issues](https://github.com/simplesamlphp/simplesamlphp/issues) to see what people are asking for. Our [roadmap](https://simplesamlphp.org/releaseplan) might also be a good place to start if you do not know exactly how you can contribute.
When submitting a pull request, please make sure to account for:
### Coding standards
* Respect the coding standards. We try to comply with PHP's [PSR-2](http://www.php-fig.org/psr/psr-2/). Pay special attention to the rules below:
* Lines should not be longer than 80 characters.
* Use **4 spaces** instead of tabs.
* Keep the keywords in **lowercase**, including `true`, `false` and `null`.
* Make sure your classes work with *autoloading*.
* Never include a trailing `?>` in your files.
* The first line of every file must be `<?php`.
* Use namespaces if you are adding new files.
* Do not include too many changes in every commit. Commits should be focused and address one single problem or feature. By having **multiple, small commits** instead of fewer large ones, it is easier to track what you are doing, revert changes in case of an error and help you make changes if needed.
* Try to write clean **commit messages**, largely based on the [seven rules](http://chris.beams.io/posts/git-commit/):
* Write a **short** subject line, followed by a blank line and a longer explanation.
* Prefix the subject line with the component(s) changed, e.g. "docs: Update foo", or "SAML: Don't log bar twice", or "tests: Add tests for quux".
* Explain **what and why** in the commit message, not just _how_. Things that seem obvious now might become quite confusing when rediscovered years later.
### Comments, comparisons, and simplicity
* Add comments that describe why/how your code works.
* Include complete **phpdoc** documentation for every property and method you add. If you change a method or property, make sure to update the existing *phpdoc* accordingly. Do not forget to document all parameters, returned values and exceptions thrown.
* Use strict comparison operators like `===` and check for specific values when writing tests.
* Avoid big functions, long nested loops or `if` statements.
* Try to keep **backwards-compatibility**. Code that breaks current configurations and installations is difficult to deploy, and therefore we try to avoid that as much as possible.
### Unit tests
Add **unit tests** to verify that your code not only works but also keeps working over time. When adding tests, keep the same directory structure used for regular classes. Try to cover **all your code** with tests. The bigger the test coverage, the more reliable and better our library is. Read our [guidelines](TESTING.md) to learn more about tests.
### Documentation
In order to keep this library user-friendly, we ask that you add proper **documentation** explaining how to use your new feature or how your code changes things.
### Pull requests
Please follow all instructions below:
1. Submit your code as a **pull request** in github from a branch with a descriptive name in your own fork of the repository.
2. Add a meaningful, short title, and explain in detail what you did and why in the description of the *PR*.
3. Add instructions on how to test your code. We appreciate branch names like `feature/whatever-new-feature` for new features and `bug/something-not-working` for bug fixes, but this is not required.
Sometimes it can take a long time before we are able to process your pull requests. Do not get discouraged, we will eventually reach your change. Remember that by following these guidelines, you are making it easier for us to analyze your request so the process will be smooth and fast. We really appreciate you helping us out, not only with your code, but also by following these guidelines.
## Reporting bugs
Before reporting a bug, please make sure it is indeed a bug. Check [the documentation](https://simplesamlphp.org/docs/stable/) to verify what the intended behaviour is. Review the [issue tracker](https://github.com/simplesamlphp/simplesamlphp/issues) and the [pull requests](https://github.com/simplesamlphp/simplesamlphp/pulls) to see if someone has already reported the same issue.
If you are able, a pull request is much more appreciated than just a new issue. If not, please do not hesitate to open one. It is better to have just an issue report than nothing!
You can help us diagnose and fix bugs by asking and providing answers to the following questions:
* How can we reproduce the bug?
* Is it reproducible in other environments (for example, on different browsers or devices)?
* Are the steps to reproduce the bug clear? If not, can you describe how you might reproduce it?
* What tags should the bug have?
* How critical is this bug? Does it impact a large amount of users?
* Is this a security issue? If so, how severe is it? How can an attacker exploit it? Read more about security issues in the next section.
## Reporting vulnerabilities
In case you find a vulnerability in SimpleSAMLphp, or you want to confirm a possible security issue in the software, please get in touch with us through [UNINETT's CERT team](https://www.uninett.no/cert). Please use our PGP public key to encrypt any possibly sensitive data that you may need to submit. We will get back to you as soon as possible according to our working hours in Central European Time.
When reporting a security issue, please add as much information as possible to help us identify, confirm, replicate and fix the problem. In particular, remember to include the following information in your report:
* The version or versions of SimpleSAMLphp affected.
* An exact version that can be used to replicate the issue.
* Any module or modules involved in the issue.
* Any particular configuration details relevant to the setup affected.
* A detailed description and a clear and concise, step-by-step guide to allow us reproduce the issue.
* Screenshots, videos, or any other media that would help identify the issue.
* Pointers to the exact line or lines in the code where the vulnerability is supposed to be.
* Context on how you discovered the issue.
* Your own name and whether you want to be credited for the discovery or not.
Please **DO NOT** report security incidents related to systems that use SimpleSAMLphp, where this software is not the cause of the incident. Issues related to the use (or misuse) of infrastructure, misconfiguration of the software, malfunction of a particular system or user-related errors should not be reported either. If you are using SimpleSAMLphp to authenticate or login to services, but you don't know what SimpleSAMLphp is or you are not sure about the nature of the issue, please contact the organization running the service for you.
Finally, be reasonable. We'll do our best to resolve the issue according to our principles of security and transparency. Every confirmed vulnerability will be published and resolved in a timely manner. All we ask in return is that you contact us privately first in order to avoid any potential damage to those using the software.
You can find the list of security advisories we have published [here](https://simplesamlphp.org/security).
## Translations
SimpleSAMLphp is translated to many languages, though it needs constant updates from translators, as well as new translations to other languages. For the moment, translations can be contributed as **pull requests**. We are looking at better ways to translate the software that would make your life easier, so stay tuned! You can also join the [translators mailing list](http://groups.google.com/group/simplesamlphp-translation) to keep up to date on the latest news.
Before starting a new translation, decide what style you want to use, whether you want to address the user in a polite manner or not, etc. Be coherent and keep that style through all your translations. If there is already a translation and you want to complete it, make sure to keep the same style used by your fellow translators.
## Documentation
Did you find a typo in the documentation? Does something make no sense? Did we use poor english? Tell us!
Documentation is included in our own repository in *markdown* format. You can submit pull requests with fixes. If you encounter some feature that is not documented, or the documentation does not reflect the real behaviour of the library, please do not hesitate to open an issue.
Good documentation is key to make things easier for our users!
## Community
You do not feel capable of contributing with your code, but are using SimpleSAMLphp and can share your knowledge and experience? Please, do so! Join our [users mailing list](http://groups.google.com/group/simplesamlphp) and help other users when you can. Your experience might be valuable for many!

21
COPYING Executable file
View File

@@ -0,0 +1,21 @@
Copyright 2007-2015 UNINETT AS, http://www.uninett.no
SimpleSAMLphp is licensed under the CC-GNU LGPL version 2.1.
http://creativecommons.org/licenses/LGPL/2.1/
Note that some of the embedded libraries may be using other licenses.
For example xmlseclibs uses BSD license.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

459
LICENSE Executable file
View File

@@ -0,0 +1,459 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS

11
README.md Executable file
View File

@@ -0,0 +1,11 @@
SimpleSAMLphp
=============
[![Build Status](https://travis-ci.org/simplesamlphp/simplesamlphp.svg?branch=master)](https://travis-ci.org/simplesamlphp/simplesamlphp)
[![Coverage Status](https://img.shields.io/coveralls/simplesamlphp/simplesamlphp.svg)](https://coveralls.io/r/simplesamlphp/simplesamlphp)
This is the official repository of the SimpleSAMLphp software.
* [SimpleSAMLphp homepage](https://simplesamlphp.org)
* [SimpleSAMLphp Downloads](https://simplesamlphp.org/download)
Please, [contribute](CONTRIBUTING.md)!

72
TESTING.md Executable file
View File

@@ -0,0 +1,72 @@
Testing
=======
Testing your code is crucial to have a stable and good quality product.
We are therefore slowly increasing the amount of tests we perform, and
as a rule of thumb **all new code should have associated tests**. If you
want to contribute to the project with a pull request, make sure to
**include tests covering your code**. We won't accept pull requests
without tests or getting the code coverage down, except in very specific
situations.
All the tests reside in the `tests` directory. The directory structure
there replicates the main structure of the code. Each class is tested by
a class named with the same name and `Test` appended, having the same
directory structure as the original, but inside the `tests` directory.
We also use namespaces, with `SimpleSAML\Test` as the root for standard
classes, and `SimpleSAML\Test\Module\modulename` for classes located in
modules.
For example, if you want to test the `SimpleSAML\Utils\HTTP` class
located in `lib/SimpleSAML/Utils/HTTP.php`, the tests must be in a class
named `HTTPTest` implemented in
`tests/lib/SimpleSAML/Utils/HTTPTest.php`, with the following namespace
definition:
```php
namespace SimpleSAML\Test\Utils;
```
The test classes need to extend `PHPUnit\Framework\TestCase`, and inside
you can implement as many methods as you want. `phpunit` will only run
the ones prefixed with "test".
You will usually make use of the `assert*()` methods provided by
`PHPUnit\Framework\TestCase`, but you can also tell `phpunit` to expect
an exception to be thrown using *phpdoc*. For example, if you want to
ensure that the `SimpleSAML\Utils\HTTP::addURLParameters()` method
throws an exception in a specific situation:
```php
/**
* Test SimpleSAML\Utils\HTTP::addURLParameters().
*
* @expectedException \InvalidArgumentException
*/
public function testAddURLParametersInvalidParameters() {
```
Refer to [the `phpunit 4.8` documentation](https://phpunit.de/manual/4.8/en/installation.html)
for more information on how to write tests. We currently use the `phpunit 4.8`
since it is the last version to support php 5.3.
Once you have implemented your tests, you can run them locally. First,
make sure the `config` directory is **not** in the root of your
SimpleSAMLphp installation, as the tests cannot use that. Make sure
you have `phpunit` installed and run:
```sh
phpunit -c tools/phpunit/phpunit.xml
```
If your default version of `phpunit` is more recent than 4.8, you can run
the old version installed by composer
```sh
./vendor/bin/phpunit -c tools/phpunit/phpunit.xml
```
All the tests are run by our *continuous integration* platform,
[travis](https://travis-ci.org/simplesamlphp/simplesamlphp). If you are
submitting a pull request, Travis will run your tests and notify whether
your code builds or not according to them.

254
ajax/v1/ajax.php Normal file
View File

@@ -0,0 +1,254 @@
<?php header('Access-Control-Allow-Origin: *'); ?>
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/*
Run4Football v0.1 Ajax API
This file is part of Run4Football. (C)2019 Corrado Mulas - CM TLC - R&D Dept. - Ardara (SS)
Run4Football 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 3 of the License, or
(at your option) any later version.
Run4Football 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 Run4Football. If not, see <http://www.gnu.org/licenses/>.
*/
/*##########NOTE################
*/
############FINE NOTE###########
/**
* Description of r4fbApi
*
* @author corradomulas for Run PoliTo - Corso Duca degli Abruzzi 24, 10129 Torino (TO)
*/
require '/sda1/www/test.mulas.me/run4football/lib/r4fb.php';
require '/sda1/www/idp.runpolito.ml/vendor/autoload.php';
//require '/sda1/www/test.mulas.me/run4football/lib/sns.php';
require_once '/sda1/www/idp.runpolito.ml/lib/_autoload.php';
//session_start();
$auth = new \SimpleSAML\Auth\Simple('runpolito-sp');
SimpleSAML_Session::getSessionFromRequest()->cleanup();
if (!$auth->isAuthenticated())
{
// SimpleSAML_Session::getSessionFromRequest()->cleanup();
//401 e tu muto
http_response_code(401);
}
else
{
$guid = $auth->getAttributes()['guid'][0];
//inizio partita:
//inserire funzione recupero matchid da DB utilizzando id utente da Service Provider e DB
switch ($_GET['op'])
{
case "start":
if(isset($_GET['mid']))
{
if(!r4fb::startMatch($_GET['mid'], $guid)) http_response_code(500);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
http_response_code(400);
}
break;
case "instor":
//r4fb::insTorneo($tsinizio,$org,$loc,$man);
break;
case "insarb":
break;
case "insamm":
if(isset($_GET['plid']) && isset($_GET['mid']) && isset($_GET['ta']))
{
r4fb::insAmm($_GET['plid'],$_GET['mid'],$_GET['ta'], $guid);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
}
break;
case "insq":
break;
case "inspl":
break;
case "insma":
break;
case "randma":
if(isset($_GET['mid']) && isset($_GET['uid']))
{
r4fb::randStart($_GET['mid'], $_GET['uid']);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
}
break;
case "insgl":
if(isset($_GET['plid']) && isset($_GET['mid']))
{
r4fb::insGoal($_GET['plid'], $_GET['mid'], $guid);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
}
break;
case "insfa":
if(isset($_GET['sid']))
{
r4fb::insFallo($_GET['sid'], $guid);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
}
break;
case "getfa":
if(isset($_GET['sid']))
{
r4fb::getFalli($_GET['sid']);
}
else
{
echo "<pre>Parametri mancanti.</pre>";
}
break;
case "stop":
r4fb::stopMatch(r4fb::curMatch($guid), $guid);
break;
case "logout":
$auth->logout();
break;
case "genguid":
echo r4fb::guidv4();
break;
case "csms":
// snsrun::sendSMS($_GET['num'], $_GET['msg']);
break;
case "uniqid":
echo str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT);
break;
case "randstr":
echo bin2hex(openssl_random_pseudo_bytes(4));
break;
case "updpt":
echo r4fb::getPoints($_GET['mid'],$_GET['sn']);
break;
case "updfl":
echo r4fb::getFalli($_GET['sid']);
break;
case "getpls":
$pls = r4fb::fetchPlayers($squadra);
$output = array(
'status' => 'ok',
'content' => $pls,
);
print_r(json_decode(json_encode($output)));
break;
case "opvt":
if(!r4fb::toggleVote($_GET['trn'],1)) http_response_code(500);
break;
case "clvt":
if(!r4fb::toggleVote($_GET['trn'],2)) http_response_code(500);
break;
case "rcmsg":
$tipo = $_GET['tipo'];
$msgs = r4fb::TTrcvdMsgs($_GET['uid'], $tipo);
//0 tutti i messaggi
//1 da leggere
//2 letti
switch($tipo)
{
case 0:
$cat = "(tutti)";
case 1:
$cat = "(non letti)";
break;
case 2:
$cat = "(letti)";
break;
}
$max = count($msgs);
echo "
<table border='1' style='display: inline-table'>
<!-- <caption>Messaggi in arrivo</caption> -->
<thead>
<tr>
<th scope='col' colspan='2'>Lista messaggi ".$cat."</th>
</tr>
</thead>
<tbody align='center'>
<tr>
<td>Inviato</td>
<td>Mittente</td>
<td>Oggetto</td>
<td>Urgente</td>
<td>Leggi</td>
</tr>";
for($j=0; $j<$max; $j++)
// if($classf[$j]['pt_gir'] != 0)
// {
echo "<tr>
<td>".$msgs[$j]['ts_sent']."</td>
<td>".$msgs[$j]['sender']."</td>
<td>".$msgs[$j]['subject']."</td>
<td>".$msgs[$j]['prio']."</td>
<td><a href='#' onclick=\"readMsg('".$msgs[$j]['id']."')\">Leggi</a></td>
</tr>";
// }
echo "
</tbody>
<tfoot align='center'>
<tr>
<td colspan='5'></td>
</tr>
</tfoot>
</table>";
break;
default:
//echo "<pre>puppa</pre>";
http_response_code(408);
break;
}
}
?>

29
ajax/v1/ajaxpb.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
require '/sda1/www/test.mulas.me/run4football/lib/r4fb.php';
switch ($_GET['op'])
{
case "getpls":
$pls = r4fb::fetchPlayers($_GET['sq']);
$output = array(
'status' => 'ok',
'content' => $pls,
);
print_r(json_encode($pls));
//print_r(json_decode(json_encode($pls)));
break;
default:
//echo "<pre>puppa</pre>";
http_response_code(408);
break;
}

762
arbitro.php Normal file
View File

@@ -0,0 +1,762 @@
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<?php
require '/sda1/www/idp.runpolito.ml/vendor/autoload.php';
require_once '/sda1/www/idp.runpolito.ml/lib/_autoload.php';
require 'lib/r4fb.php';
//session_start();
$auth = new \SimpleSAML\Auth\Simple('runpolito-sp');
/*if (!$auth->isAuthenticated()) {
SimpleSAML_Session::getSessionFromRequest()->cleanup();
// Show login link.
print('<a href="/login">Login</a>');
}*/
//Auth livello 1:
$auth->requireAuth();
SimpleSAML_Session::getSessionFromRequest()->cleanup();
//print_r($auth->getAttributes());
$guid = $auth->getAttributes()['guid'][0];
//print_r(r4fb::fetchUsers($guid));
$nome = r4fb::fetchUsers($guid)[0]['nome'];
$cognome = r4fb::fetchUsers($guid)[0]['cognome'];
$matr = r4fb::fetchUsers($guid)[0]['matr'];
//Auth livello 2:
$tipo = trim(r4fb::fetchUsers($guid)[0]['tipo']);
//print $tipo;
if($tipo != 1 && $tipo != 3)
{
$auth->logout();
header("Location: index.php?error=Privilegi insufficienti.");
die("su cunnu e tzia rua bagassa");
}
?>
<?php header('Access-Control-Allow-Origin: https://api.runpolito.ml/'); ?>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link rel='stylesheet' href='style/arbitro.css'/>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Archivo" rel="stylesheet">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body id="body">
<!-- <script type="text/javascript">
//Script gestione AJAX su eventi onClick()
function startMatch(matchid)
{
$.ajax(
{
url:"https://api.runpolito.ml/torneo/ajax/v1/ajax.php",
type:"GET",
data:{start: 1, id: matchid}
success:function(result)
{
$("div#stato").html("Partita iniziata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
})
}
</script> -->
<div id="header">
<?php
/*
Run4Football v0.1
This file is part of Run4Football. (C)2019 Corrado Mulas - CM TLC - R&D Dept. - Ardara (SS)
Run4Football 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 3 of the License, or
(at your option) any later version.
Run4Football 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 Run4Football. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Description of arbitro
*
* @author corradomulas for Run PoliTo - Corso Duca degli Abruzzi 24, 10129 Torino (TO)
*/
// put your code here
//MAIN:
//ins. codice auth e recupera $torneo da sessione
// echo r4fb::insTorneo(0,"merda","merda","merda");
//$torneo lo prendi da guid, l'arbitro è assegnato ad uno e un solo torneo
// print("<p>Ciao, ".$nome." ".$cognome." <button value='logout' onclick='logout()'>Logout</button></p>");
$torneo = r4fb::curTourn($guid);
$nometorn = r4fb::tournData($torneo)['nome'];
echo "<span id='clock' onload='showTime()' style='margin-left:2%'></span><span style='margin-left:30%'>R: ".$matr."</span> "."<span style='float:right'>P1/1 C1&nbsp;&nbsp;&nbsp;T: ".$nometorn."</span>";
//$torneo = '0D3E9767-EDA6-447C-B925-145CDA2D75D1';
// r4fb::insMatch(10, 'merda1s1', 'merda1s2', 'bastard1', $torneo);
// r4fb::insMatch(10, 'merda2s1', 'merda2s2', 'bastard2', $torneo);
?>
</div>
<div class="maincont">
<div class="stato"></div>
<div class="points wtext">
<h2>Attendere aggiornamento prima di ripetere operazione</h2>
<span class="falli">Falli&nbsp;</span><span class="falli" id="falli1">0</span>&nbsp;&nbsp;<strong id="points"><span id="pts1">0</span>-<span id="pts2">0</span></strong>&nbsp;&nbsp;<span class="falli" id="falli2">0</span>
<span class="falli">&nbsp;Falli</span>
</div>
<!-- Sez A elenco partite (solo su tablet/PC): se su mobile, visualizza solo start partita imminente -->
<div class="match">
<ul class="partite">
<?php
/* DATI ESEMPIO:
Array
(
[0] => Array
(
[id] => 31A97136-B893-4612-8E45-5DC9957302A3
[t_prog] => 10
[t_start] =>
[t_stop] => 0
[s1] => merda1s1
[s2] => merda1s2
[pt_s1] => 0
[pt_s2] => 0
[pt] => 0
[falli] => 0
[arb] => bastard1
[torneo] => 0D3E9767-EDA6-447C-B925-145CDA2D75D1
)
[1] => Array
(
[id] => FCED463D-59A8-4D58-8D6B-BE37CCAF3593
[t_prog] => 10
[t_start] =>
[t_stop] => 0
[s1] => merda2s1
[s2] => merda2s2
[pt_s1] => 0
[pt_s2] => 0
[pt] => 0
[falli] => 0
[arb] => bastard2
[torneo] => 0D3E9767-EDA6-447C-B925-145CDA2D75D1
)
)
*
*/
//echo r4fb::curMatch($guid);
$partite = r4fb::fetchPartite($torneo,$guid);
//echo '<pre>'; print_r($partite); echo '</pre>';
$max = sizeof($partite); //limitare res a prime 5 non disputate
for($i = 0; $i < $max;$i++)
{
$marray = $partite[$i];
//servono id e ts_prog: il punteggio viene aggiornato dinamicamente via AJAX
$id = $marray['id'];
$t_prog = $marray['t_prog'];
//<strong id="points">0-0</strong>
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
echo '<li><span class="wtext">'.date('H:i', $t_prog).'</span><button class="startmatch btn1" value="Start" onclick="startMatch(\''.$id.'\')">start</button>'
.'<button class="stopmatch btn1" onclick="stopMatch()">Termina</button></li>';
}
?>
</ul>
<!-- fine sez A -->
<!-- Sez B su due colonne - comandi -->
<div class="giocatori">
<?php
$mid = r4fb::curMatch($guid); //setta matchid su DB su utente arbitro al momento dello start della partita
$uid = $guid; //id utente loggato, da prendere da SAML
//occorre prendere entrambe le sq dal punto precedente!
//r4fb::insSq($torneo, "cagatuppas", "#FF0000");
//r4fb::insSq($torneo, "cagatuppas2", "#FF1234");
//r4fb::insPlayer(1, 1, "Sto cazzo", "staqminchia.jpg", "");
/*r4fb::insPlayer(1, 1, "Sto cazzo", "staqminchia.jpg", "1B6582D9-4A03-437E-BB2B-B40007539963");
r4fb::insPlayer(2, 2, "Sto cazzo2", "staqminchia.jpg", "1B6582D9-4A03-437E-BB2B-B40007539963");
r4fb::insPlayer(3, 3, "Sto cazzo3", "staqminchia.jpg", "1B6582D9-4A03-437E-BB2B-B40007539963");
r4fb::insPlayer(1, 1, "Sta minchia", "staqminchia.jpg", "8BA9DD80-B8EB-4E4A-B098-DBC14D475B89");
r4fb::insPlayer(2, 2, "Sta minchia2", "staqminchia.jpg", "8BA9DD80-B8EB-4E4A-B098-DBC14D475B89");
r4fb::insPlayer(3, 3, "Sta minchia3", "staqminchia.jpg", "8BA9DD80-B8EB-4E4A-B098-DBC14D475B89");*/
//trova $squadre, hai curMatch!
$s1 = r4fb::fetchSquadre($mid)['s1'];
$s2 = r4fb::fetchSquadre($mid)['s2'];
$squadra = "8BA9DD80-B8EB-4E4A-B098-DBC14D475B89";
$players = r4fb::fetchPlayers($s1);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
// echo '<strong id="points">0-0</strong>';
echo "<div class='pli' style='float:left; margin-left: 20%'><ul class='wtext' id='squadra1' >Squadra 1";
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
if($parray['isActive'])
{
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
echo '<li >'.'<button class="goal btn1" value="'.$id.'" onclick="insGoal(\''.$mid.'\',\''.$id.'\')">'.$parray['nome'].'</button></li>';
}
}
echo '<li><button class="fallo btn1" onclick="insFallo(\''.$s1.'\')">Fallo</button></li>'."</ul></div>";
$players = r4fb::fetchPlayers($s2);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
echo "<div class='pli' style='float:right; margin-right: 20%'><ul class='wtext' id='squadra2'>Squadra 2";
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
if($parray['isActive'])
{
echo '<li >'.'<button class="goal btn1" value="'.$id.'" onclick="insGoal(\''.$mid.'\',\''.$id.'\')">'.$parray['nome'].'</button></li>';
}
}
echo '<li><button class="fallo btn1" onclick="insFallo(\''.$s2.'\')">Fallo</button></li>'."</ul></div>";
$players = r4fb::fetchPlayers($s1);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
echo "<div class='ammonizione' style='float:left'><ul class='wtext' id='squadra1'>Squadra 1";
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
if($parray['isActive'])
{
echo '<li ><fieldset>'
. '<button class="ammonizione btn1" style="background-color:yellow;" value="'.$id.'" onclick="insAmm(\''.$mid.'\',\''.$id.'\',\'1\')">'.$parray['nome'].'</button>'
. '<button class="ammonizione btn1" style="background-color:red;" value="'.$id.'" onclick="insAmm(\''.$mid.'\',\''.$id.'\',\'2\')">'.$parray['nome'].'</button></fieldset></li>';
}
}
echo "</ul></div>";
$players = r4fb::fetchPlayers($s2);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
echo "<div class='ammonizione' style='float:right; margin-right:20%;'><ul class='wtext' id='squadra2'>Squadra 2";
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
if($parray['isActive'])
{
echo '<li ><fieldset>'
. '<button class="ammonizione btn1" style="background-color:yellow;" value="'.$id.'" onclick="insAmm(\''.$mid.'\',\''.$id.'\',\'1\')">'.$parray['nome'].'</button>'
. '<button class="ammonizione btn1" style="background-color:red;" value="'.$id.'" onclick="insAmm(\''.$mid.'\',\''.$id.'\',\'2\')">'.$parray['nome'].'</button></fieldset></li>';
}
}
echo "</ul></div>";
?>
</div>
<!-- fine sez B -->
<!-- Sez C - comando termine -->
<ul class="termine">
<?php
echo '';
?>
</ul>
<!-- fine sez C -->
<!-- Sez D - footer -->
<ul id="footer">
<?php
?>
</ul>
<!-- fine sez D -->
<?php
?>
</div>
<div class="emgtab">Richiesta assistenza/soccorso <br />
<button class="btn1" onclick="logout()" style="width: 6vw"></button>
<button class="btn1" onclick="logout()" style="width: 6vw"></button>
<button class="btn1" onclick="logout()" style="width: 6vw"></button>
</div>
<div class="usertab">Utente<br />
<ul>
<li><button class="btn1" onclick="logout()" style="width: 6vw">Logout</button></li>
<li><button class="btn1" onclick="logout()" style="width: 6vw">Cambia password</button></li>
<li><button class="btn1" onclick="logout()" style="width: 6vw">Chiedi sostituzione</button></li>
</ul>
</div>
<div class="startab">Inizio/termine partita<br />
</div>
<div class="selpls">
<form name="selpls" method="post" action="">
<?php
echo "<div class='selpls' style=''><ul class='wtext' id='squadra1' >Squadra 1<li>";
$players = r4fb::fetchPlayers($s1);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
echo '<label for="abpl1">'.$parray['nome'].'<input class="chkbx" type="checkbox" name="abpl1[]" value="'.$id.'"/>'.'</label>';
}
echo "</li>";
//echo '<li><button class="fallo btn1" onclick="insFallo(\''.$s1.'\')">Fallo</button></li>'."</ul>";
$players = r4fb::fetchPlayers($s2);
//echo '<pre>'; print_r($players); echo '</pre>';
$max = sizeof($players);
// echo "<div class='selpls' style=''>";
echo "<ul class='wtext' id='squadra2'>Squadra 2<li>";
for($i = 0; $i < $max;$i++)
{
$parray = $players[$i];
$id = $parray['id'];
//print pulsante di start con id, converti ts_prog in hh:mm, print campi risultato senza risultato (0 - 0)
echo '<label for="abpl1">'.$parray['nome'].'<input class="chkbx" type="checkbox" name="abpl1[]" value="'.$id.'"/>'.'</label>';
}
echo '</li><li><input type="submit" name="selpl" value="Seleziona giocatori"></li>'."</ul></div>";
if(isset($_POST['selpl']))
{
r4fb::togglePls($_POST['abpl1'],$s1,$s2);
print_r(count($_POST['abpl1'])); echo "<br />";
//print_r($_POST['abpl2']);
}
?>
</form>
</div>
<div class="msgtab">
<a href="#" onclick="">Tutti</a> <a href="#" onclick="">Non letti</a> <a href="#" onclick="">Letti</a>
<div id="rcvdmsgs"></div>
</div>
</div> <!-- fine main container div -->
<div id="footer">
<a href="#" onclick="emgTab()"><i class="func fas fa-life-ring"></i></a>
<a href="#" onclick="toggleBg('body')"><i class="func fas fa-moon"></i></a>
<a href="#" onclick="starTab()"><i class="func fas fa-play"></i></a>
<a href="#" onclick="matchTab()"><i class="func fas fa-futbol"></i></a>
<a href="#" onclick="ammTab()"><i class="func fas fa-thumbs-down"></i></a>
<a href="#" onclick="userTab()"><i class="func fas fa-user"></i></a>
<a href="#" onclick="msgTab()"><i class="func fas fa-envelope"></i></a>
<a href="#" onclick="selplsTab()"><i class="func fas fa-running"></i></a>
</div>
<script type="text/javascript">
window.setInterval(function(){
/// call your function here
updPoints();
}, 2000);
function getMsgs(uid, tipo)
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "rcmsg", uid: uid, tipo: tipo},
success:function(result)
{
$("div#rcvdmsgs").html(result);
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
window.location.href = 'logout.php';
}
//Script gestione AJAX su eventi onClick()
//eccetto per start, matchid si recupera da DB
function startMatch(matchid)
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "start", mid: matchid},
//data:{op: "start"},
success:function(result)
{
$("div#stato").html("Partita iniziata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
//jQuery(".startmatch").css('display','none');
//jQuery(".stopmatch").css('display','inline-block');
document.location.reload();
document.location.reload();
}
function stopMatch()
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "stop"},
success:function(result)
{
$("div#stato").html("Partita terminata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
document.location.reload();
document.location.reload();
}
function insFallo(sid)
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "insfa", sid: sid},
success:function(data)
{
if(data == "0")
{
$("div#stato").html("Fallo inserito"+data);
}
else if(data == "1")
{
$("div#stato").html("Quinto fallo cons.: Tiro libero previsto"+data);
}
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
}
function insAmm(matchid, plid, ta)
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "insamm", mid: matchid, plid: plid, ta: ta},
success:function(result)
{
$("div#stato").html("Partita terminata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
}
function insGoal(matchid, plid)
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "insgl", mid: matchid, plid: plid},
success:function(result)
{
$("div#stato").html("Partita terminata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
}
function logout()
{
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "logout"},
success:function(result)
{
$("div#stato").html("Partita terminata");
},
error:function(stato,errori)
{
$("div#stato").html("Errore"+stato+" "+errori);
}
});
window.location.href = 'logout.php';
}
function updPoints() {
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "updpt", mid:"<?php echo $mid?>", sn: "1"},
success:function(result)
{
jQuery("#pts1").text(result);
},
});
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "updpt", mid:"<?php echo $mid?>", sn: "2"},
success:function(result)
{
jQuery("#pts2").text(result);
},
});
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "updfl", sid:"<?php echo $s1?>"},
success:function(result)
{
jQuery("#falli1").text(result);
},
});
$.ajax(
{
url:"ajax/v1/ajax.php",
type:"GET",
data:{op: "updfl", sid:"<?php echo $s2?>"},
success:function(result)
{
jQuery("#falli2").text(result);
},
});
}
</script>
<!--JS grafici-->
<script language="JavaScript">
function toggleBg(id) {
var state = document.getElementById(id).style.backgroundColor;
if (state == 'black') {
$(id).css("background-color","whitesmoke");
jQuery(".wtext").css('color','black');
jQuery("#header").css('border-bottom-color','black');
} else {
$(id).css("background-color","black");
jQuery(".wtext").css('color','white');
jQuery("#header").css('border-bottom-color','white');
}
}
function emgTab()
{
jQuery(".match").css('display','none');
jQuery(".emgtab").css('display','block');
jQuery(".usertab").css('display','none');
jQuery(".msgtab").css('display','none');
jQuery(".selpls").css('display','none');
}
function userTab()
{
jQuery(".match").css('display','none');
jQuery(".usertab").css('display','block');
jQuery(".emgtab").css('display','none');;
jQuery(".selpls").css('display','none');
}
function msgTab()
{
jQuery(".match").css('display','none');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');;
jQuery(".selpls").css('display','none');
jQuery(".msgtab").css('display','block');
jQuery("#points").css('display','none');
jQuery(".fallo").css('display','none');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');
jQuery(".fallo").css('display','none');
jQuery(".goal").css('display','none');
jQuery(".pli").css('display','none');
jQuery(".ammonizione").css('display','none');
jQuery(".startmatch").css('display','none');
jQuery(".stopmatch").css('display','none');
jQuery(".points").css('display','none');
jQuery("#partite").css('display','none');
jQuery(".partite").css('display','none');
jQuery(".selpls").css('display','none');
}
function matchTab()
{
jQuery(".match").css('display','block');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');
jQuery(".msgtab").css('display','none');
jQuery(".fallo").css('display','block');
jQuery(".goal").css('display','block');
jQuery(".pli").css('display','block');
jQuery(".ammonizione").css('display','none');
jQuery(".startmatch").css('display','none');
jQuery(".stopmatch").css('display','none');
jQuery("#points").css('display','inline-block');
jQuery("#partite").css('display','none');
jQuery(".partite").css('display','none');
jQuery(".selpls").css('display','none');
}
function ammTab()
{
jQuery(".match").css('display','block');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');
jQuery(".ammonizione").css('display','block');
jQuery(".fallo").css('display','none');
jQuery(".goal").css('display','none');
jQuery(".pli").css('display','none');
jQuery(".partite").css('display','none');
jQuery("#points").css('display','none');
jQuery(".msgtab").css('display','none');
jQuery(".selpls").css('display','none');
//#fallo #ammonizione #goal #pli #partite
}
function starTab()
{
jQuery("#match").css('display','block');
jQuery(".startmatch").css('display','inline-block');
jQuery(".stopmatch").css('display','inline-block');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');
jQuery(".ammonizione").css('display','none');
jQuery(".fallo").css('display','none');
jQuery(".goal").css('display','none');
jQuery(".pli").css('display','none');
jQuery("#partite").css('display','inline-block');
jQuery(".partite").css('display','inline-block');
jQuery("#points").css('display','none');
jQuery(".msgtab").css('display','none');
jQuery(".selpls").css('display','none');
//#fallo #ammonizione #goal #pli #partite
}
function selplsTab()
{
jQuery(".selpls").css('display','inline-block');
jQuery("#match").css('display','none');
jQuery(".startmatch").css('display','none');
jQuery(".stopmatch").css('display','none');
jQuery(".usertab").css('display','none');
jQuery(".emgtab").css('display','none');
jQuery(".ammonizione").css('display','none');
jQuery(".fallo").css('display','none');
jQuery(".goal").css('display','none');
jQuery(".pli").css('display','none');
jQuery("#partite").css('display','none');
jQuery(".partite").css('display','none');
jQuery("#points").css('display','none');
jQuery(".msgtab").css('display','none');
//#fallo #ammonizione #goal #pli #partite
}
</script>
<script type="text/javascript">
function showTime(){
var date = new Date();
var h = date.getHours(); // 0 - 23
var m = date.getMinutes(); // 0 - 59
var s = date.getSeconds(); // 0 - 59
//var session = "PM";
var session = "";
/* if(h == 0){
h = 12;
}
if(h > 12){
h = h - 12;
session = "AM";
} */
h = (h < 10) ? "0" + h : h;
m = (m < 10) ? "0" + m : m;
s = (s < 10) ? "0" + s : s;
var time = h + ":" + m + ":" + s + " " + session;
document.getElementById("clock").innerText = time;
document.getElementById("clock").textContent = time;
setTimeout(showTime, 1000);
}
showTime();
</script>
</body>
</html>

32
auth.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/*
Run4Football v0.1
This file is part of Run4Football. (C)2019 Corrado Mulas - CM TLC - R&D Dept. - Ardara (SS)
Run4Football 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 3 of the License, or
(at your option) any later version.
Run4Football 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 Run4Football. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Description of auth
*
* @author corradomulas for Run PoliTo - Corso Duca degli Abruzzi 24, 10129 Torino (TO)
*/

90
giocatore.php Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
/*
Run4Football v0.1
This file is part of Run4Football. (C)2019 Corrado Mulas - CM TLC - R&D Dept. - Ardara (SS)
Run4Football 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 3 of the License, or
(at your option) any later version.
Run4Football 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 Run4Football. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Description of giocatore
*
* @author corradomulas for Run PoliTo - Corso Duca degli Abruzzi 24, 10129 Torino (TO)
*/
// put your code here
//MAIN:
require '/sda1/www/idp.runpolito.ml/vendor/autoload.php';
require_once '/sda1/www/idp.runpolito.ml/lib/_autoload.php';
require 'lib/r4fb.php';
//require 'lib/sns.php';
//session_start();
$auth = new \SimpleSAML\Auth\Simple('runpolito-sp');
/*if (!$auth->isAuthenticated()) {
SimpleSAML_Session::getSessionFromRequest()->cleanup();
// Show login link.
print('<a href="/login">Login</a>');
}*/
//Auth livello 1:
$auth->requireAuth();
SimpleSAML_Session::getSessionFromRequest()->cleanup();
//print_r($auth->getAttributes());
$guid = $auth->getAttributes()['guid'][0];
//print_r(r4fb::fetchUsers($guid));
$nome = r4fb::fetchUsers($guid)[0]['nome'];
$cognome = r4fb::fetchUsers($guid)[0]['cognome'];
$matr = r4fb::fetchUsers($guid)[0]['matr'];
//Auth livello 2:
$tipo = trim(r4fb::fetchUsers($guid)[0]['tipo']);
//print $tipo;
if($tipo != 1 && $tipo != 4)
{
$auth->logout();
header("Location: index.php?error=Privilegi insufficienti.");
die("su cunnu e tzia rua bagassa");
}
else
{
echo "Ciao merda"
. "<form name='logout' action='' method='post'>"
. "<input type='submit' name='logout' value='Logout' />"
. "</form>";
}
if(isset($_POST['logout']))
{
$auth->logout();
}
?>
</body>
</html>

45
index.php Normal file
View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<!--
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
-->
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<?php
/*
Run4Football v0.1
This file is part of Run4Football. (C)2019 Corrado Mulas - CM TLC - R&D Dept. - Ardara (SS)
Run4Football 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 3 of the License, or
(at your option) any later version.
Run4Football 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 Run4Football. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Description of index
*
* @author corradomulas for Run PoliTo - Corso Duca degli Abruzzi 24, 10129 Torino (TO)
*/
// put your code here
//MAIN:
?>
</body>
</html>

1855
js/jscolor.js Normal file

File diff suppressed because it is too large Load Diff

139
lib/SimpleSAML/Auth/Default.php Executable file
View File

@@ -0,0 +1,139 @@
<?php
/**
* Implements the default behaviour for authentication.
*
* This class contains an implementation for default behaviour when authenticating. It will
* save the session information it got from the authentication client in the users session.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*
* @deprecated This class will be removed in SSP 2.0.
*/
class SimpleSAML_Auth_Default
{
/**
* @deprecated This method will be removed in SSP 2.0. Use SimpleSAML_Auth_Source::initLogin() instead.
*/
public static function initLogin(
$authId,
$return,
$errorURL = null,
array $params = array()
) {
$as = self::getAuthSource($authId);
$as->initLogin($return, $errorURL, $params);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML_Auth_State::getPersistentAuthData() instead.
*/
public static function extractPersistentAuthState(array &$state)
{
return SimpleSAML_Auth_State::getPersistentAuthData($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_Source::loginCompleted() instead.
*/
public static function loginCompleted($state)
{
SimpleSAML_Auth_Source::loginCompleted($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function initLogoutReturn($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
$session = SimpleSAML_Session::getSessionFromRequest();
$state = $session->getAuthData($authority, 'LogoutState');
$session->doLogout($authority);
$state['SimpleSAML_Auth_Default.ReturnURL'] = $returnURL;
$state['LogoutCompletedHandler'] = array(get_class(), 'logoutCompleted');
$as = SimpleSAML_Auth_Source::getById($authority);
if ($as === null) {
// The authority wasn't an authentication source...
self::logoutCompleted($state);
}
$as->logout($state);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function initLogout($returnURL, $authority)
{
assert(is_string($returnURL));
assert(is_string($authority));
self::initLogoutReturn($returnURL, $authority);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($returnURL);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Default.ReturnURL', $state));
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['SimpleSAML_Auth_Default.ReturnURL']);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_Source::logoutCallback() instead.
*/
public static function logoutCallback($state)
{
SimpleSAML_Auth_Source::logoutCallback($state);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth() instead.
*/
public static function handleUnsolicitedAuth($authId, array $state, $redirectTo)
{
sspmod_saml_Auth_Source_SP::handleUnsolicitedAuth($authId, $state, $redirectTo);
}
/**
* Return an authentication source by ID.
*
* @param string $id The id of the authentication source.
* @return SimpleSAML_Auth_Source The authentication source.
* @throws Exception If the $id does not correspond with an authentication source.
*/
private static function getAuthSource($id)
{
$as = SimpleSAML_Auth_Source::getById($id);
if ($as === null) {
throw new Exception('Invalid authentication source: ' . $id);
}
return $as;
}
}

775
lib/SimpleSAML/Auth/LDAP.php Executable file
View File

@@ -0,0 +1,775 @@
<?php
/**
* Constants defining possible errors
*/
define('ERR_INTERNAL', 1);
define('ERR_NO_USER', 2);
define('ERR_WRONG_PW', 3);
define('ERR_AS_DATA_INCONSIST', 4);
define('ERR_AS_INTERNAL', 5);
define('ERR_AS_ATTRIBUTE', 6);
// not defined in earlier PHP versions
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
}
/**
* The LDAP class holds helper functions to access an LDAP database.
*
* @author Andreas Aakre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Anders Lund, UNINETT AS. <anders.lund@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_LDAP
{
/**
* LDAP link identifier.
*
* @var resource
*/
protected $ldap = null;
/**
* LDAP user: authz_id if SASL is in use, binding dn otherwise
*/
protected $authz_id = null;
/**
* Timeout value, in seconds.
*
* @var int
*/
protected $timeout = 0;
/**
* Private constructor restricts instantiation to getInstance().
*
* @param string $hostname
* @param bool $enable_tls
* @param bool $debug
* @param int $timeout
* @param int $port
* @param bool $referrals
*/
public function __construct($hostname, $enable_tls = true, $debug = false, $timeout = 0, $port = 389, $referrals = true)
{
// Debug
SimpleSAML\Logger::debug('Library - LDAP __construct(): Setup LDAP with '.
'host=\''.$hostname.
'\', tls='.var_export($enable_tls, true).
', debug='.var_export($debug, true).
', timeout='.var_export($timeout, true).
', referrals='.var_export($referrals, true));
/*
* Set debug level before calling connect. Note that this passes
* NULL to ldap_set_option, which is an undocumented feature.
*
* OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
*/
if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
}
/*
* Prepare a connection for to this LDAP server. Note that this function
* doesn't actually connect to the server.
*/
$this->ldap = @ldap_connect($hostname, $port);
if ($this->ldap === false) {
throw $this->makeException('Library - LDAP __construct(): Unable to connect to \''.$hostname.'\'', ERR_INTERNAL);
}
// Enable LDAP protocol version 3
if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', ERR_INTERNAL);
}
// Set referral option
if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals, ERR_INTERNAL);
}
// Set timeouts, if supported
// (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
$this->timeout = $timeout;
if ($timeout > 0) {
if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to '.$timeout);
}
if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to '.$timeout);
}
}
// Enable TLS, if needed
if (stripos($hostname, "ldaps:") === false && $enable_tls) {
if (!@ldap_start_tls($this->ldap)) {
throw $this->makeException('Library - LDAP __construct(): Unable to force TLS', ERR_INTERNAL);
}
}
}
/**
* Convenience method to create an LDAPException as well as log the
* description.
*
* @param string $description
* The exception's description
* @return Exception
*/
private function makeException($description, $type = null)
{
$errNo = 0x00;
// Log LDAP code and description, if possible
if (empty($this->ldap)) {
SimpleSAML\Logger::error($description);
} else {
$errNo = @ldap_errno($this->ldap);
}
// Decide exception type and return
if ($type) {
if ($errNo !== 0) {
// Only log real LDAP errors; not success
SimpleSAML\Logger::error($description.'; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')');
} else {
SimpleSAML\Logger::error($description);
}
switch ($type) {
case ERR_INTERNAL:// 1 - ExInternal
return new SimpleSAML_Error_Exception($description, $errNo);
case ERR_NO_USER:// 2 - ExUserNotFound
return new SimpleSAML_Error_UserNotFound($description, $errNo);
case ERR_WRONG_PW:// 3 - ExInvalidCredential
return new SimpleSAML_Error_InvalidCredential($description, $errNo);
case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
return new SimpleSAML_Error_AuthSource('ldap', $description);
case ERR_AS_INTERNAL:// 5 - ExAsInternal
return new SimpleSAML_Error_AuthSource('ldap', $description);
}
} else {
if ($errNo !== 0) {
$description .= '; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')';
if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) && !empty($extendedError)) {
$description .= '; additional: \''.$extendedError.'\'';
}
}
switch ($errNo) {
case 0x20://LDAP_NO_SUCH_OBJECT
SimpleSAML\Logger::warning($description);
return new SimpleSAML_Error_UserNotFound($description, $errNo);
case 0x31://LDAP_INVALID_CREDENTIALS
SimpleSAML\Logger::info($description);
return new SimpleSAML_Error_InvalidCredential($description, $errNo);
case -1://NO_SERVER_CONNECTION
SimpleSAML\Logger::error($description);
return new SimpleSAML_Error_AuthSource('ldap', $description);
default:
SimpleSAML\Logger::error($description);
return new SimpleSAML_Error_AuthSource('ldap', $description);
}
}
}
/**
* Search for DN from a single base.
*
* @param string $base
* Indication of root of subtree to search
* @param string|array $attribute
* The attribute name(s) to search for.
* @param string $value
* The attribute value to search for.
* Additional search filter
* @param string|null $searchFilter
* The scope of the search
* @param string $scope
* @return string
* The DN of the resulting found element.
* @throws SimpleSAML_Error_Exception if:
* - Attribute parameter is wrong type
* @throws SimpleSAML_Error_AuthSource if:
* - Not able to connect to LDAP server
* - False search result
* - Count return false
* - Searche found more than one result
* - Failed to get first entry from result
* - Failed to get DN for entry
* @throws SimpleSAML_Error_UserNotFound if:
* - Zero entries were found
*/
private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree")
{
// Create the search filter
$attribute = self::escape_filter_value($attribute, false);
$value = self::escape_filter_value($value, true);
$filter = '';
foreach ($attribute as $attr) {
$filter .= '('.$attr.'='.$value.')';
}
$filter = '(|'.$filter.')';
// Append LDAP filters if defined
if ($searchFilter != null) {
$filter = "(&".$filter."".$searchFilter.")";
}
// Search using generated filter
SimpleSAML\Logger::debug('Library - LDAP search(): Searching base ('.$scope.') \''.$base.'\' for \''.$filter.'\'');
if ($scope === 'base') {
$result = @ldap_read($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
} else if ($scope === 'onelevel') {
$result = @ldap_list($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
} else {
$result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
}
if ($result === false) {
throw $this->makeException('Library - LDAP search(): Failed search on base \''.$base.'\' for \''.$filter.'\'');
}
// Sanity checks on search results
$count = @ldap_count_entries($this->ldap, $result);
if ($count === false) {
throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
} elseif ($count > 1) {
// More than one entry is found. External error
throw $this->makeException('Library - LDAP search(): Found '.$count.' entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_AS_DATA_INCONSIST);
} elseif ($count === 0) {
// No entry is fond => wrong username is given (or not registered in the catalogue). User error
throw $this->makeException('Library - LDAP search(): Found no entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_NO_USER);
}
// Resolve the DN from the search result
$entry = @ldap_first_entry($this->ldap, $result);
if ($entry === false) {
throw $this->makeException('Library - LDAP search(): Unable to retrieve result after searching base \''.$base.'\' for \''.$filter.'\'');
}
$dn = @ldap_get_dn($this->ldap, $entry);
if ($dn === false) {
throw $this->makeException('Library - LDAP search(): Unable to get DN after searching base \''.$base.'\' for \''.$filter.'\'');
}
return $dn;
}
/**
* Search for a DN.
*
* @param string|array $base
* The base, or bases, which to search from.
* @param string|array $attribute
* The attribute name(s) searched for.
* @param string $value
* The attribute value searched for.
* @param bool $allowZeroHits
* Determines if the method will throw an exception if no hits are found.
* Defaults to FALSE.
* @param string|null $searchFilter
* Additional searchFilter to be added to the (attribute=value) filter
* @param string $scope
* The scope of the search
* @return string
* The DN of the matching element, if found. If no element was found and
* $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
* NULL will be returned.
* @throws SimpleSAML_Error_AuthSource if:
* - LDAP search encounter some problems when searching cataloge
* - Not able to connect to LDAP server
* @throws SimpleSAML_Error_UserNotFound if:
* - $allowZeroHits is FALSE and no result is found
*
*/
public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null, $scope = 'subtree')
{
// Traverse all search bases, returning DN if found
$bases = SimpleSAML\Utils\Arrays::arrayize($base);
foreach ($bases as $current) {
try {
// Single base search
$result = $this->search($current, $attribute, $value, $searchFilter, $scope);
// We don't hawe to look any futher if user is found
if (!empty($result)) {
return $result;
}
// If search failed, attempt the other base DNs
} catch (SimpleSAML_Error_UserNotFound $e) {
// Just continue searching
}
}
// Decide what to do for zero entries
SimpleSAML\Logger::debug('Library - LDAP searchfordn(): No entries found');
if ($allowZeroHits) {
// Zero hits allowed
return null;
} else {
// Zero hits not allowed
throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for filter \'('.
join(' | ', $attribute).' = '.$value.')\' on base(s) \'('.join(' & ', $bases).')\'', 2);
}
}
/**
* This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
* method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
*
* @throws Exception
* @param string|array $bases
* @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter, or a raw filter string
* @param string|array $attributes Array of attributes requested from LDAP
* @param bool $and If multiple filters defined, then either bind them with & or |
* @param bool $escape Weather to escape the filter values or not
* @param string $scope The scope of the search
* @return array
*/
public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true, $scope = 'subtree')
{
// Escape the filter values, if requested
if ($escape) {
$filters = $this->escape_filter_value($filters, false);
}
// Build search filter
$filter = '';
if (is_array($filters)) {
foreach ($filters as $attribute => $value) {
$filter .= "($attribute=$value)";
}
if (count($filters) > 1) {
$filter = ($and ? '(&' : '(|').$filter.')';
}
} elseif (is_string($filters)) {
$filter = $filters;
}
// Verify filter was created
if ($filter == '' || $filter == '(=)') {
throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
}
// Verify at least one base was passed
$bases = (array) $bases;
if (empty($bases)) {
throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
}
// Search each base until result is found
$result = false;
foreach ($bases as $base) {
if ($scope === 'base') {
$result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
} else if ($scope === 'onelevel') {
$result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
} else {
$result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
}
if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
break;
}
}
// Verify that a result was found in one of the bases
if ($result === false) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) ['.
implode('; ', $bases).'] with filter ['.$filter.']. LDAP error ['.
ldap_error($this->ldap).']'
);
} elseif (@ldap_count_entries($this->ldap, $result) < 1) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) ['.
implode('; ', $bases).'] with filter ['.$filter.']',
ERR_NO_USER
);
}
// Get all results
$results = ldap_get_entries($this->ldap, $result);
if ($results === false) {
throw $this->makeException(
'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
);
}
// parse each entry and process its attributes
for ($i = 0; $i < $results['count']; $i++) {
$entry = $results[$i];
// iterate over the attributes of the entry
for ($j = 0; $j < $entry['count']; $j++) {
$name = $entry[$j];
$attribute = $entry[$name];
// decide whether to base64 encode or not
for ($k = 0; $k < $attribute['count']; $k++) {
// base64 encode binary attributes
if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid') {
$results[$i][$name][$k] = base64_encode($attribute[$k]);
}
}
}
}
// Remove the count and return
unset($results['count']);
return $results;
}
/**
* Bind to LDAP with a specific DN and password. Simple wrapper around
* ldap_bind() with some additional logging.
*
* @param string $dn
* The DN used.
* @param string $password
* The password used.
* @param array $sasl_args
* Array of SASL options for SASL bind
* @return bool
* Returns TRUE if successful, FALSE if
* LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
* LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
* @throws SimpleSAML_Error_Exception on other errors
*/
public function bind($dn, $password, array $sasl_args = null)
{
if ($sasl_args != null) {
if (!function_exists('ldap_sasl_bind')) {
$ex_msg = 'Library - missing SASL support';
throw $this->makeException($ex_msg);
}
// SASL Bind, with error handling
$authz_id = $sasl_args['authz_id'];
$error = @ldap_sasl_bind(
$this->ldap,
$dn,
$password,
$sasl_args['mech'],
$sasl_args['realm'],
$sasl_args['authc_id'],
$sasl_args['authz_id'],
$sasl_args['props']
);
} else {
// Simple Bind, with error handling
$authz_id = $dn;
$error = @ldap_bind($this->ldap, $dn, $password);
}
if ($error === true) {
// Good
$this->authz_id = $authz_id;
SimpleSAML\Logger::debug('Library - LDAP bind(): Bind successful with DN \''.$dn.'\'');
return true;
}
/* Handle errors
* LDAP_INVALID_CREDENTIALS
* LDAP_INSUFFICIENT_ACCESS */
switch (ldap_errno($this->ldap)) {
case 32: // LDAP_NO_SUCH_OBJECT
// no break
case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
// no break
case 48: // LDAP_INAPPROPRIATE_AUTH
// no break
case 49: // LDAP_INVALID_CREDENTIALS
// no break
case 50: // LDAP_INSUFFICIENT_ACCESS
return false;
default:
break;
}
// Bad
throw $this->makeException('Library - LDAP bind(): Bind failed with DN \''.$dn.'\'');
}
/**
* Applies an LDAP option to the current connection.
*
* @throws Exception
* @param $option
* @param $value
* @return void
*/
public function setOption($option, $value)
{
// Attempt to set the LDAP option
if (!@ldap_set_option($this->ldap, $option, $value)) {
throw $this->makeException(
'ldap:LdapConnection->setOption : Failed to set LDAP option ['.
$option.'] with the value ['.$value.'] error: '.ldap_error($this->ldap),
ERR_INTERNAL
);
}
// Log debug message
SimpleSAML\Logger::debug(
'ldap:LdapConnection->setOption : Set the LDAP option ['.
$option.'] with the value ['.$value.']'
);
}
/**
* Search a given DN for attributes, and return the resulting associative
* array.
*
* @param string $dn
* The DN of an element.
* @param string|array $attributes
* The names of the attribute(s) to retrieve. Defaults to NULL; that is,
* all available attributes. Note that this is not very effective.
* @param int $maxsize
* The maximum size of any attribute's value(s). If exceeded, the attribute
* will not be returned.
* @return array
* The array of attributes and their values.
* @see http://no.php.net/manual/en/function.ldap-read.php
*/
public function getAttributes($dn, $attributes = null, $maxsize = null)
{
// Preparations, including a pretty debug message...
$description = 'all attributes';
if (is_array($attributes)) {
$description = '\''.join(',', $attributes).'\'';
} else {
// Get all attributes...
// TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
$attributes = array();
}
SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Getting '.$description.' from DN \''.$dn.'\'');
// Attempt to get attributes
// TODO: Should aliases be dereferenced?
$result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
if ($result === false) {
throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \''.$dn.'\'');
}
$entry = @ldap_first_entry($this->ldap, $result);
if ($entry === false) {
throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \''.$dn.'\'');
}
$attributes = @ldap_get_attributes($this->ldap, $entry); // Recycling $attributes... Possibly bad practice.
if ($attributes === false) {
throw $this->makeException('Library - LDAP getAttributes(): Could not get attributes of first entry from DN \''.$dn.'\'');
}
// Parsing each found attribute into our result set
$result = array(); // Recycling $result... Possibly bad practice.
for ($i = 0; $i < $attributes['count']; $i++) {
// Ignore attributes that exceed the maximum allowed size
$name = $attributes[$i];
$attribute = $attributes[$name];
// Deciding whether to base64 encode
$values = array();
for ($j = 0; $j < $attribute['count']; $j++) {
$value = $attribute[$j];
if (!empty($maxsize) && strlen($value) > $maxsize) {
// Ignoring and warning
SimpleSAML\Logger::warning('Library - LDAP getAttributes(): Attribute \''.
$name.'\' exceeded maximum allowed size by '.(strlen($value) - $maxsize));
continue;
}
// Base64 encode binary attributes
if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid' || strtolower($name) === 'ms-ds-consistencyguid') {
$values[] = base64_encode($value);
} else {
$values[] = $value;
}
}
// Adding
$result[$name] = $values;
}
// We're done
SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Found attributes \'('.join(',', array_keys($result)).')\'');
return $result;
}
/**
* Enter description here...
*
* @param array $config
* @param string $username
* @param string $password
* @return array|bool
*/
public function validate($config, $username, $password = null)
{
/* Escape any characters with a special meaning in LDAP. The following
* characters have a special meaning (according to RFC 2253):
* ',', '+', '"', '\', '<', '>', ';', '*'
* These characters are escaped by prefixing them with '\'.
*/
$username = addcslashes($username, ',+"\\<>;*');
if (isset($config['priv_user_dn'])) {
$this->bind($config['priv_user_dn'], $config['priv_user_pw']);
}
if (isset($config['dnpattern'])) {
$dn = str_replace('%username%', $username, $config['dnpattern']);
} else {
$dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username);
}
if ($password !== null) { // checking users credentials ... assuming below that she may read her own attributes ...
// escape characters with a special meaning, also in the password
$password = addcslashes($password, ',+"\\<>;*');
if (!$this->bind($dn, $password)) {
SimpleSAML\Logger::info('Library - LDAP validate(): Failed to authenticate \''.$username.'\' using DN \''.$dn.'\'');
return false;
}
}
/*
* Retrieve attributes from LDAP
*/
$attributes = $this->getAttributes($dn, $config['attributes']);
return $attributes;
}
/**
* Borrowed function from PEAR:LDAP.
*
* Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
*
* Any control characters with an ACII code < 32 as well as the characters with special meaning in
* LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
* backslash followed by two hex digits representing the hexadecimal value of the character.
*
* @static
* @param string|array $values Array of values to escape
* @return array Array $values, but escaped
*/
public static function escape_filter_value($values = array(), $singleValue = true)
{
// Parameter validation
$values = \SimpleSAML\Utils\Arrays::arrayize($values);
foreach ($values as $key => $val) {
// Escaping of filter meta characters
$val = str_replace('\\', '\5c', $val);
$val = str_replace('*', '\2a', $val);
$val = str_replace('(', '\28', $val);
$val = str_replace(')', '\29', $val);
// ASCII < 32 escaping
$val = self::asc2hex32($val);
if (null === $val) {
$val = '\0'; // apply escaped "null" if string is empty
}
$values[$key] = $val;
}
if ($singleValue) {
return $values[0];
}
return $values;
}
/**
* Borrowed function from PEAR:LDAP.
*
* Converts all ASCII chars < 32 to "\HEX"
*
* @param string $string String to convert
*
* @static
* @return string
*/
public static function asc2hex32($string)
{
for ($i = 0; $i < strlen($string); $i++) {
$char = substr($string, $i, 1);
if (ord($char) < 32) {
$hex = dechex(ord($char));
if (strlen($hex) == 1) {
$hex = '0'.$hex;
}
$string = str_replace($char, '\\'.$hex, $string);
}
}
return $string;
}
/**
* Convert SASL authz_id into a DN
*/
private function authzid_to_dn($searchBase, $searchAttributes, $authz_id)
{
if (preg_match("/^dn:/", $authz_id)) {
return preg_replace("/^dn:/", "", $authz_id);
}
if (preg_match("/^u:/", $authz_id)) {
return $this->searchfordn(
$searchBase,
$searchAttributes,
preg_replace("/^u:/", "", $authz_id)
);
}
return $authz_id;
}
/**
* ldap_exop_whoami accessor, if available. Use requested authz_id
* otherwise.
*
* ldap_exop_whoami() has been provided as a third party patch that
* waited several years to get its way upstream:
* http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/databases/php-ldap/files
*
* When it was integrated into PHP repository, the function prototype
* was changed, The new prototype was used in third party patch for
* PHP 7.0 and 7.1, hence the version test below.
*/
public function whoami($searchBase, $searchAttributes)
{
$authz_id = '';
if (function_exists('ldap_exop_whoami')) {
if (version_compare(phpversion(), '7', '<')) {
if (ldap_exop_whoami($this->ldap, $authz_id) !== true) {
throw $this->makeException('LDAP whoami exop failure');
}
} else {
if (($authz_id = ldap_exop_whoami($this->ldap)) === false) {
throw $this->makeException('LDAP whoami exop failure');
}
}
} else {
$authz_id = $this->authz_id;
}
$dn = $this->authzid_to_dn($searchBase, $searchAttributes, $authz_id);
if (!isset($dn) || ($dn == '')) {
throw $this->makeException('Cannot figure userID');
}
return $dn;
}
}

View File

@@ -0,0 +1,369 @@
<?php
/**
* Class for implementing authentication processing chains for IdPs.
*
* This class implements a system for additional steps which should be taken by an IdP before
* submitting a response to a SP. Examples of additional steps can be additional authentication
* checks, or attribute consent requirements.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_ProcessingChain
{
/**
* The list of remaining filters which should be applied to the state.
*/
const FILTERS_INDEX = 'SimpleSAML_Auth_ProcessingChain.filters';
/**
* The stage we use for completed requests.
*/
const COMPLETED_STAGE = 'SimpleSAML_Auth_ProcessingChain.completed';
/**
* The request parameter we will use to pass the state identifier when we redirect after
* having completed processing of the state.
*/
const AUTHPARAM = 'AuthProcId';
/**
* All authentication processing filters, in the order they should be applied.
*/
private $filters;
/**
* Initialize an authentication processing chain for the given service provider
* and identity provider.
*
* @param array $idpMetadata The metadata for the IdP.
* @param array $spMetadata The metadata for the SP.
*/
public function __construct($idpMetadata, $spMetadata, $mode = 'idp')
{
assert(is_array($idpMetadata));
assert(is_array($spMetadata));
$this->filters = array();
$config = SimpleSAML_Configuration::getInstance();
$configauthproc = $config->getArray('authproc.' . $mode, null);
if (!empty($configauthproc)) {
$configfilters = self::parseFilterList($configauthproc);
self::addFilters($this->filters, $configfilters);
}
if (array_key_exists('authproc', $idpMetadata)) {
$idpFilters = self::parseFilterList($idpMetadata['authproc']);
self::addFilters($this->filters, $idpFilters);
}
if (array_key_exists('authproc', $spMetadata)) {
$spFilters = self::parseFilterList($spMetadata['authproc']);
self::addFilters($this->filters, $spFilters);
}
SimpleSAML\Logger::debug('Filter config for ' . $idpMetadata['entityid'] . '->' .
$spMetadata['entityid'] . ': ' . str_replace("\n", '', var_export($this->filters, true)));
}
/**
* Sort & merge filter configuration
*
* Inserts unsorted filters into sorted filter list. This sort operation is stable.
*
* @param array &$target Target filter list. This list must be sorted.
* @param array $src Source filters. May be unsorted.
*/
private static function addFilters(&$target, $src)
{
assert(is_array($target));
assert(is_array($src));
foreach ($src as $filter) {
$fp = $filter->priority;
// Find insertion position for filter
for ($i = count($target)-1; $i >= 0; $i--) {
if ($target[$i]->priority <= $fp) {
// The new filter should be inserted after this one
break;
}
}
/* $i now points to the filter which should preceede the current filter. */
array_splice($target, $i+1, 0, array($filter));
}
}
/**
* Parse an array of authentication processing filters.
*
* @param array $filterSrc Array with filter configuration.
* @return array Array of SimpleSAML_Auth_ProcessingFilter objects.
*/
private static function parseFilterList($filterSrc)
{
assert(is_array($filterSrc));
$parsedFilters = array();
foreach ($filterSrc as $priority => $filter) {
if (is_string($filter)) {
$filter = array('class' => $filter);
}
if (!is_array($filter)) {
throw new Exception('Invalid authentication processing filter configuration: ' .
'One of the filters wasn\'t a string or an array.');
}
$parsedFilters[] = self::parseFilter($filter, $priority);
}
return $parsedFilters;
}
/**
* Parse an authentication processing filter.
*
* @param array $config Array with the authentication processing filter configuration.
* @param int $priority The priority of the current filter, (not included in the filter
* definition.)
* @return SimpleSAML_Auth_ProcessingFilter The parsed filter.
*/
private static function parseFilter($config, $priority)
{
assert(is_array($config));
if (!array_key_exists('class', $config)) {
throw new Exception('Authentication processing filter without name given.');
}
$className = SimpleSAML\Module::resolveClass($config['class'], 'Auth_Process', 'SimpleSAML_Auth_ProcessingFilter');
$config['%priority'] = $priority;
unset($config['class']);
return new $className($config, null);
}
/**
* Process the given state.
*
* This function will only return if processing completes. If processing requires showing
* a page to the user, we will not be able to return from this function. There are two ways
* this can be handled:
* - Redirect to a URL: We will redirect to the URL set in $state['ReturnURL'].
* - Call a function: We will call the function set in $state['ReturnCall'].
*
* If an exception is thrown during processing, it should be handled by the caller of
* this function. If the user has redirected to a different page, the exception will be
* returned through the exception handler defined on the state array. See
* SimpleSAML_Auth_State for more information.
*
* @see SimpleSAML_Auth_State
* @see SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL
* @see SimpleSAML_Auth_State::EXCEPTION_HANDLER_FUNC
*
* @param array &$state The state we are processing.
*/
public function processState(&$state)
{
assert(is_array($state));
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
$state[self::FILTERS_INDEX] = $this->filters;
try {
// TODO: remove this in SSP 2.0
if (!array_key_exists('UserID', $state)) {
// No unique user ID present. Attempt to add one.
self::addUserID($state);
}
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
$filter->process($state);
}
} catch (SimpleSAML_Error_Exception $e) {
// No need to convert the exception
throw $e;
} catch (Exception $e) {
/*
* To be consistent with the exception we return after an redirect,
* we convert this exception before returning it.
*/
throw new SimpleSAML_Error_UnserializableException($e);
}
// Completed
}
/**
* Continues processing of the state.
*
* This function is used to resume processing by filters which for example needed to show
* a page to the user.
*
* This function will never return. Exceptions thrown during processing will be passed
* to whatever exception handler is defined in the state array.
*
* @param array $state The state we are processing.
*/
public static function resumeProcessing($state)
{
assert(is_array($state));
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
}
// Completed
assert(array_key_exists('ReturnURL', $state) || array_key_exists('ReturnCall', $state));
assert(!array_key_exists('ReturnURL', $state) || !array_key_exists('ReturnCall', $state));
if (array_key_exists('ReturnURL', $state)) {
/*
* Save state information, and redirect to the URL specified
* in $state['ReturnURL'].
*/
$id = SimpleSAML_Auth_State::saveState($state, self::COMPLETED_STAGE);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['ReturnURL'], array(self::AUTHPARAM => $id));
} else {
/* Pass the state to the function defined in $state['ReturnCall']. */
// We are done with the state array in the session. Delete it.
SimpleSAML_Auth_State::deleteState($state);
$func = $state['ReturnCall'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
}
/**
* Process the given state passivly.
*
* Modules with user interaction are expected to throw an \SimpleSAML\Module\saml\Error\NoPassive exception
* which are silently ignored. Exceptions of other types are passed further up the call stack.
*
* This function will only return if processing completes.
*
* @param array &$state The state we are processing.
*/
public function processStatePassive(&$state)
{
assert(is_array($state));
// Should not be set when calling this method
assert(!array_key_exists('ReturnURL', $state));
// Notify filters about passive request
$state['isPassive'] = true;
$state[self::FILTERS_INDEX] = $this->filters;
// TODO: remove this in SSP 2.0
if (!array_key_exists('UserID', $state)) {
// No unique user ID present. Attempt to add one.
self::addUserID($state);
}
while (count($state[self::FILTERS_INDEX]) > 0) {
$filter = array_shift($state[self::FILTERS_INDEX]);
try {
$filter->process($state);
// Ignore SimpleSAML_Error_NoPassive exceptions
} catch (SimpleSAML_Error_NoPassive $e) {
// @deprecated will be removed in 2.0
// Ignore \SimpleSAML\Error\NoPassive exceptions
} catch (\SimpleSAML\Module\saml\Error\NoPassive $e) {
// Ignore \SimpleSAML\Module\saml\Error\NoPassive exceptions
}
}
}
/**
* Retrieve a state which has finished processing.
*
* @param string $id The state identifier.
* @see SimpleSAML_Auth_State::parseStateID()
* @return Array The state referenced by the $id parameter.
*/
public static function fetchProcessedState($id)
{
assert(is_string($id));
return SimpleSAML_Auth_State::loadState($id, self::COMPLETED_STAGE);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
private static function addUserID(&$state)
{
assert(is_array($state));
assert(array_key_exists('Attributes', $state));
if (isset($state['Destination']['userid.attribute'])) {
$attributeName = $state['Destination']['userid.attribute'];
SimpleSAML\Logger::warning("The 'userid.attribute' option has been deprecated.");
} elseif (isset($state['Source']['userid.attribute'])) {
$attributeName = $state['Source']['userid.attribute'];
SimpleSAML\Logger::warning("The 'userid.attribute' option has been deprecated.");
} else {
// Default attribute
$attributeName = 'eduPersonPrincipalName';
}
if (!array_key_exists($attributeName, $state['Attributes'])) {
return;
}
$uid = $state['Attributes'][$attributeName];
if (count($uid) === 0) {
SimpleSAML\Logger::warning('Empty user id attribute [' . $attributeName . '].');
return;
}
if (count($uid) > 1) {
SimpleSAML\Logger::warning('Multiple attribute values for user id attribute [' . $attributeName . '].');
return;
}
// TODO: the attribute value should be trimmed
$uid = $uid[0];
if (empty($uid)) {
SimpleSAML\Logger::warning('Empty value in attribute '.$attributeName.". on user. Cannot set UserID.");
return;
}
$state['UserID'] = $uid;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Base class for authentication processing filters.
*
* All authentication processing filters must support serialization.
*
* The current request is stored in an associative array. It has the following defined attributes:
* - 'Attributes' The attributes of the user.
* - 'Destination' Metadata of the destination (SP).
* - 'Source' Metadata of the source (IdP).
*
* It may also contain other attributes. If an authentication processing filter wishes to store other
* information in it, it should have a name on the form 'module:filter:attributename', to avoid name
* collisions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Auth_ProcessingFilter
{
/**
* Priority of this filter.
*
* Used when merging IdP and SP processing chains.
* The priority can be any integer. The default for most filters is 50. Filters may however
* specify their own default, if they typically should be amongst the first or the last filters.
*
* The prioroty can also be overridden by the user by specifying the '%priority' option.
*/
public $priority = 50;
/**
* Constructor for a processing filter.
*
* Any processing filter which implements its own constructor must call this
* constructor first.
*
* @param array &$config Configuration for this filter.
* @param mixed $reserved For future use.
*/
public function __construct(&$config, $reserved)
{
assert(is_array($config));
if (array_key_exists('%priority', $config)) {
$this->priority = $config['%priority'];
if (!is_int($this->priority)) {
throw new Exception('Invalid priority: ' . var_export($this->priority, true));
}
unset($config['%priority']);
}
}
/**
* Process a request.
*
* When a filter returns from this function, it is assumed to have completed its task.
*
* @param array &$request The request we are currently processing.
*/
abstract public function process(&$request);
}

401
lib/SimpleSAML/Auth/Simple.php Executable file
View File

@@ -0,0 +1,401 @@
<?php
namespace SimpleSAML\Auth;
use \SimpleSAML_Auth_Source as Source;
use \SimpleSAML_Auth_State as State;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML_Error_AuthSource as AuthSourceError;
use \SimpleSAML\Module;
use \SimpleSAML_Session as Session;
use \SimpleSAML\Utils\HTTP;
/**
* Helper class for simple authentication applications.
*
* @package SimpleSAMLphp
*/
class Simple
{
/**
* The id of the authentication source we are accessing.
*
* @var string
*/
protected $authSource;
/**
* @var \SimpleSAML_Configuration|null
*/
protected $app_config;
/**
* Create an instance with the specified authsource.
*
* @param string $authSource The id of the authentication source.
*/
public function __construct($authSource)
{
assert(is_string($authSource));
$this->authSource = $authSource;
$this->app_config = Configuration::getInstance()->getConfigItem('application', null);
}
/**
* Retrieve the implementing authentication source.
*
* @return \SimpleSAML_Auth_Source The authentication source.
*
* @throws \SimpleSAML_Error_AuthSource If the requested auth source is unknown.
*/
public function getAuthSource()
{
$as = Source::getById($this->authSource);
if ($as === null) {
throw new AuthSourceError($this->authSource, 'Unknown authentication source.');
}
return $as;
}
/**
* Check if the user is authenticated.
*
* This function checks if the user is authenticated with the default authentication source selected by the
* 'default-authsource' option in 'config.php'.
*
* @return bool True if the user is authenticated, false if not.
*/
public function isAuthenticated()
{
$session = Session::getSessionFromRequest();
return $session->isValid($this->authSource);
}
/**
* Require the user to be authenticated.
*
* If the user is authenticated, this function returns immediately.
*
* If the user isn't authenticated, this function will authenticate the user with the authentication source, and
* then return the user to the current page.
*
* This function accepts an array $params, which controls some parts of the authentication. See the login()
* method for a description.
*
* @param array $params Various options to the authentication request. See the documentation.
*/
public function requireAuth(array $params = array())
{
$session = Session::getSessionFromRequest();
if ($session->isValid($this->authSource)) {
// Already authenticated
return;
}
$this->login($params);
}
/**
* Start an authentication process.
*
* This function accepts an array $params, which controls some parts of the authentication. The accepted parameters
* depends on the authentication source being used. Some parameters are generic:
* - 'ErrorURL': A URL that should receive errors from the authentication.
* - 'KeepPost': If the current request is a POST request, keep the POST data until after the authentication.
* - 'ReturnTo': The URL the user should be returned to after authentication.
* - 'ReturnCallback': The function we should call after the user has finished authentication.
*
* Please note: this function never returns.
*
* @param array $params Various options to the authentication request.
*/
public function login(array $params = array())
{
if (array_key_exists('KeepPost', $params)) {
$keepPost = (bool) $params['KeepPost'];
} else {
$keepPost = true;
}
if (array_key_exists('ReturnTo', $params)) {
$returnTo = (string) $params['ReturnTo'];
} else {
if (array_key_exists('ReturnCallback', $params)) {
$returnTo = (array) $params['ReturnCallback'];
} else {
$returnTo = HTTP::getSelfURL();
}
}
if (is_string($returnTo) && $keepPost && $_SERVER['REQUEST_METHOD'] === 'POST') {
$returnTo = HTTP::getPOSTRedirectURL($returnTo, $_POST);
}
if (array_key_exists('ErrorURL', $params)) {
$errorURL = (string) $params['ErrorURL'];
} else {
$errorURL = null;
}
if (!isset($params[State::RESTART]) && is_string($returnTo)) {
/*
* A URL to restart the authentication, in case the user bookmarks
* something, e.g. the discovery service page.
*/
$restartURL = $this->getLoginURL($returnTo);
$params[State::RESTART] = $restartURL;
}
$as = $this->getAuthSource();
$as->initLogin($returnTo, $errorURL, $params);
assert(false);
}
/**
* Log the user out.
*
* This function logs the user out. It will never return. By default, it will cause a redirect to the current page
* after logging the user out, but a different URL can be given with the $params parameter.
*
* Generic parameters are:
* - 'ReturnTo': The URL the user should be returned to after logout.
* - 'ReturnCallback': The function that should be called after logout.
* - 'ReturnStateParam': The parameter we should return the state in when redirecting.
* - 'ReturnStateStage': The stage the state array should be saved with.
*
* @param string|array|null $params Either the URL the user should be redirected to after logging out, or an array
* with parameters for the logout. If this parameter is null, we will return to the current page.
*/
public function logout($params = null)
{
assert(is_array($params) || is_string($params) || $params === null);
if ($params === null) {
$params = HTTP::getSelfURL();
}
if (is_string($params)) {
$params = array(
'ReturnTo' => $params,
);
}
assert(is_array($params));
assert(isset($params['ReturnTo']) || isset($params['ReturnCallback']));
if (isset($params['ReturnStateParam']) || isset($params['ReturnStateStage'])) {
assert(isset($params['ReturnStateParam'], $params['ReturnStateStage']));
}
$session = Session::getSessionFromRequest();
if ($session->isValid($this->authSource)) {
$state = $session->getAuthData($this->authSource, 'LogoutState');
if ($state !== null) {
$params = array_merge($state, $params);
}
$session->doLogout($this->authSource);
$params['LogoutCompletedHandler'] = array(get_class(), 'logoutCompleted');
$as = Source::getById($this->authSource);
if ($as !== null) {
$as->logout($params);
}
}
self::logoutCompleted($params);
}
/**
* Called when logout operation completes.
*
* This function never returns.
*
* @param array $state The state after the logout.
*/
public static function logoutCompleted($state)
{
assert(is_array($state));
assert(isset($state['ReturnTo']) || isset($state['ReturnCallback']));
if (isset($state['ReturnCallback'])) {
call_user_func($state['ReturnCallback'], $state);
assert(false);
} else {
$params = array();
if (isset($state['ReturnStateParam']) || isset($state['ReturnStateStage'])) {
assert(isset($state['ReturnStateParam'], $state['ReturnStateStage']));
$stateID = State::saveState($state, $state['ReturnStateStage']);
$params[$state['ReturnStateParam']] = $stateID;
}
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['ReturnTo'], $params);
}
}
/**
* Retrieve attributes of the current user.
*
* This function will retrieve the attributes of the current user if the user is authenticated. If the user isn't
* authenticated, it will return an empty array.
*
* @return array The users attributes.
*/
public function getAttributes()
{
if (!$this->isAuthenticated()) {
// Not authenticated
return array();
}
// Authenticated
$session = Session::getSessionFromRequest();
return $session->getAuthData($this->authSource, 'Attributes');
}
/**
* Retrieve authentication data.
*
* @param string $name The name of the parameter, e.g. 'Attributes', 'Expire' or 'saml:sp:IdP'.
*
* @return mixed|null The value of the parameter, or null if it isn't found or we are unauthenticated.
*/
public function getAuthData($name)
{
assert(is_string($name));
if (!$this->isAuthenticated()) {
return null;
}
$session = Session::getSessionFromRequest();
return $session->getAuthData($this->authSource, $name);
}
/**
* Retrieve all authentication data.
*
* @return array|null All persistent authentication data, or null if we aren't authenticated.
*/
public function getAuthDataArray()
{
if (!$this->isAuthenticated()) {
return null;
}
$session = Session::getSessionFromRequest();
return $session->getAuthState($this->authSource);
}
/**
* Retrieve a URL that can be used to log the user in.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLoginURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
$login = Module::getModuleURL('core/as_login.php', array(
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
));
return $login;
}
/**
* Retrieve a URL that can be used to log the user out.
*
* @param string|null $returnTo The page the user should be returned to afterwards. If this parameter is null, the
* user will be returned to the current page.
*
* @return string A URL which is suitable for use in link-elements.
*/
public function getLogoutURL($returnTo = null)
{
assert($returnTo === null || is_string($returnTo));
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
$logout = Module::getModuleURL('core/as_logout.php', array(
'AuthId' => $this->authSource,
'ReturnTo' => $returnTo,
));
return $logout;
}
/**
* Process a URL and modify it according to the application/baseURL configuration option, if present.
*
* @param string|null $url The URL to process, or null if we want to use the current URL. Both partial and full
* URLs can be used as a parameter. The maximum precedence is given to the application/baseURL configuration option,
* then the URL specified (if it specifies scheme, host and port) and finally the environment observed in the
* server.
*
* @return string The URL modified according to the precedence rules.
*/
protected function getProcessedURL($url = null)
{
if ($url === null) {
$url = HTTP::getSelfURL();
}
$scheme = parse_url($url, PHP_URL_SCHEME);
$host = parse_url($url, PHP_URL_HOST) ?: HTTP::getSelfHost();
$port = parse_url($url, PHP_URL_PORT) ?: (
$scheme ? '' : trim(HTTP::getServerPort(), ':')
);
$scheme = $scheme ?: (HTTP::getServerHTTPS() ? 'https' : 'http');
$path = parse_url($url, PHP_URL_PATH) ?: '/';
$query = parse_url($url, PHP_URL_QUERY) ?: '';
$fragment = parse_url($url, PHP_URL_FRAGMENT) ?: '';
$port = !empty($port) ? ':'.$port : '';
if (($scheme === 'http' && $port === ':80') || ($scheme === 'https' && $port === ':443')) {
$port = '';
}
if (is_null($this->app_config)) {
// nothing more we can do here
return $scheme.'://'.$host.$port.$path.($query ? '?'.$query : '').($fragment ? '#'.$fragment : '');
}
$base = trim($this->app_config->getString(
'baseURL',
$scheme.'://'.$host.$port
), '/');
return $base.$path.($query ? '?'.$query : '').($fragment ? '#'.$fragment : '');
}
}

513
lib/SimpleSAML/Auth/Source.php Executable file
View File

@@ -0,0 +1,513 @@
<?php
use SimpleSAML\Auth\SourceFactory;
/**
* This class defines a base class for authentication source.
*
* An authentication source is any system which somehow authenticate the user.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Auth_Source
{
/**
* The authentication source identifier. This identifier can be used to look up this object, for example when
* returning from a login form.
*
* @var string
*/
protected $authId;
/**
* Constructor for an authentication source.
*
* Any authentication source which implements its own constructor must call this
* constructor first.
*
* @param array $info Information about this authentication source.
* @param array &$config Configuration for this authentication source.
*/
public function __construct($info, &$config)
{
assert(is_array($info));
assert(is_array($config));
assert(array_key_exists('AuthId', $info));
$this->authId = $info['AuthId'];
}
/**
* Get sources of a specific type.
*
* @param string $type The type of the authentication source.
*
* @return SimpleSAML_Auth_Source[] Array of SimpleSAML_Auth_Source objects of the specified type.
* @throws Exception If the authentication source is invalid.
*/
public static function getSourcesOfType($type)
{
assert(is_string($type));
$config = SimpleSAML_Configuration::getConfig('authsources.php');
$ret = array();
$sources = $config->getOptions();
foreach ($sources as $id) {
$source = $config->getArray($id);
self::validateSource($source, $id);
if ($source[0] !== $type) {
continue;
}
$ret[] = self::parseAuthSource($id, $source);
}
return $ret;
}
/**
* Retrieve the ID of this authentication source.
*
* @return string The ID of this authentication source.
*/
public function getAuthId()
{
return $this->authId;
}
/**
* Process a request.
*
* If an authentication source returns from this function, it is assumed to have
* authenticated the user, and should have set elements in $state with the attributes
* of the user.
*
* If the authentication process requires additional steps which make it impossible to
* complete before returning from this function, the authentication source should
* save the state, and at a later stage, load the state, update it with the authentication
* information about the user, and call completeAuth with the state array.
*
* @param array &$state Information about the current authentication.
*/
abstract public function authenticate(&$state);
/**
* Reauthenticate an user.
*
* This function is called by the IdP to give the authentication source a chance to
* interact with the user even in the case when the user is already authenticated.
*
* @param array &$state Information about the current authentication.
*/
public function reauthenticate(array &$state)
{
assert(isset($state['ReturnCallback']));
// the default implementation just copies over the previous authentication data
$session = SimpleSAML_Session::getSessionFromRequest();
$data = $session->getAuthState($this->authId);
foreach ($data as $k => $v) {
$state[$k] = $v;
}
}
/**
* Complete authentication.
*
* This function should be called if authentication has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
*/
public static function completeAuth(&$state)
{
assert(is_array($state));
assert(array_key_exists('LoginCompletedHandler', $state));
SimpleSAML_Auth_State::deleteState($state);
$func = $state['LoginCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Start authentication.
*
* This method never returns.
*
* @param string|array $return The URL or function we should direct the user to after authentication. If using a
* URL obtained from user input, please make sure to check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param string|null $errorURL The URL we should direct the user to after failed authentication. Can be null, in
* which case a standard error page will be shown. If using a URL obtained from user input, please make sure to
* check it by calling \SimpleSAML\Utils\HTTP::checkURLAllowed().
* @param array $params Extra information about the login. Different authentication requestors may provide different
* information. Optional, will default to an empty array.
*/
public function initLogin($return, $errorURL = null, array $params = array())
{
assert(is_string($return) || is_array($return));
assert(is_string($errorURL) || $errorURL === null);
$state = array_merge($params, array(
'SimpleSAML_Auth_Default.id' => $this->authId, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.id' => $this->authId,
'SimpleSAML_Auth_Default.Return' => $return, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.Return' => $return,
'SimpleSAML_Auth_Default.ErrorURL' => $errorURL, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.ErrorURL' => $errorURL,
'LoginCompletedHandler' => array(get_class(), 'loginCompleted'),
'LogoutCallback' => array(get_class(), 'logoutCallback'),
'LogoutCallbackState' => array(
'SimpleSAML_Auth_Default.logoutSource' => $this->authId, // TODO: remove in 2.0
'SimpleSAML_Auth_Source.logoutSource' => $this->authId,
),
));
if (is_string($return)) {
$state['SimpleSAML_Auth_Default.ReturnURL'] = $return; // TODO: remove in 2.0
$state['SimpleSAML_Auth_Source.ReturnURL'] = $return;
}
if ($errorURL !== null) {
$state[SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL] = $errorURL;
}
try {
$this->authenticate($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
self::loginCompleted($state);
}
/**
* Called when a login operation has finished.
*
* This method never returns.
*
* @param array $state The state after the login has completed.
*/
public static function loginCompleted($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Source.Return', $state));
assert(array_key_exists('SimpleSAML_Auth_Source.id', $state));
assert(array_key_exists('Attributes', $state));
assert(!array_key_exists('LogoutState', $state) || is_array($state['LogoutState']));
$return = $state['SimpleSAML_Auth_Source.Return'];
// save session state
$session = SimpleSAML_Session::getSessionFromRequest();
$authId = $state['SimpleSAML_Auth_Source.id'];
$session->doLogin($authId, SimpleSAML_Auth_State::getPersistentAuthData($state));
if (is_string($return)) { // redirect...
\SimpleSAML\Utils\HTTP::redirectTrustedURL($return);
} else {
call_user_func($return, $state);
}
assert(false);
}
/**
* Log out from this authentication source.
*
* This function should be overridden if the authentication source requires special
* steps to complete a logout operation.
*
* If the logout process requires a redirect, the state should be saved. Once the
* logout operation is completed, the state should be restored, and completeLogout
* should be called with the state. If this operation can be completed without
* showing the user a page, or redirecting, this function should return.
*
* @param array &$state Information about the current logout operation.
*/
public function logout(&$state)
{
assert(is_array($state));
// default logout handler which doesn't do anything
}
/**
* Complete logout.
*
* This function should be called after logout has completed. It will never return,
* except in the case of exceptions. Exceptions thrown from this page should not be caught,
* but should instead be passed to the top-level exception handler.
*
* @param array &$state Information about the current authentication.
*/
public static function completeLogout(&$state)
{
assert(is_array($state));
assert(array_key_exists('LogoutCompletedHandler', $state));
SimpleSAML_Auth_State::deleteState($state);
$func = $state['LogoutCompletedHandler'];
assert(is_callable($func));
call_user_func($func, $state);
assert(false);
}
/**
* Create authentication source object from configuration array.
*
* This function takes an array with the configuration for an authentication source object,
* and returns the object.
*
* @param string $authId The authentication source identifier.
* @param array $config The configuration.
*
* @return SimpleSAML_Auth_Source The parsed authentication source.
* @throws Exception If the authentication source is invalid.
*/
private static function parseAuthSource($authId, $config)
{
assert(is_string($authId));
assert(is_array($config));
self::validateSource($config, $authId);
$id = $config[0];
$info = array('AuthId' => $authId);
$authSource = null;
unset($config[0]);
try {
// Check whether or not there's a factory responsible for instantiating our Auth Source instance
$factoryClass = SimpleSAML\Module::resolveClass($id, 'Auth_Source_Factory', 'SimpleSAML\Auth\SourceFactory');
/** @var SourceFactory $factory */
$factory = new $factoryClass;
$authSource = $factory->create($info, $config);
} catch (Exception $e) {
// If not, instantiate the Auth Source here
$className = SimpleSAML\Module::resolveClass($id, 'Auth_Source', 'SimpleSAML_Auth_Source');
$authSource = new $className($info, $config);
}
return $authSource;
}
/**
* Retrieve authentication source.
*
* This function takes an id of an authentication source, and returns the
* AuthSource object. If no authentication source with the given id can be found,
* NULL will be returned.
*
* If the $type parameter is specified, this function will return an
* authentication source of the given type. If no authentication source or if an
* authentication source of a different type is found, an exception will be thrown.
*
* @param string $authId The authentication source identifier.
* @param string|NULL $type The type of authentication source. If NULL, any type will be accepted.
*
* @return SimpleSAML_Auth_Source|NULL The AuthSource object, or NULL if no authentication
* source with the given identifier is found.
* @throws SimpleSAML_Error_Exception If no such authentication source is found or it is invalid.
*/
public static function getById($authId, $type = null)
{
assert(is_string($authId));
assert($type === null || is_string($type));
// for now - load and parse config file
$config = SimpleSAML_Configuration::getConfig('authsources.php');
$authConfig = $config->getArray($authId, null);
if ($authConfig === null) {
if ($type !== null) {
throw new SimpleSAML_Error_Exception(
'No authentication source with id '.
var_export($authId, true).' found.'
);
}
return null;
}
$ret = self::parseAuthSource($authId, $authConfig);
if ($type === null || $ret instanceof $type) {
return $ret;
}
// the authentication source doesn't have the correct type
throw new SimpleSAML_Error_Exception(
'Invalid type of authentication source '.
var_export($authId, true).'. Was '.var_export(get_class($ret), true).
', should be '.var_export($type, true).'.'
);
}
/**
* Called when the authentication source receives an external logout request.
*
* @param array $state State array for the logout operation.
*/
public static function logoutCallback($state)
{
assert(is_array($state));
assert(array_key_exists('SimpleSAML_Auth_Source.logoutSource', $state));
$source = $state['SimpleSAML_Auth_Source.logoutSource'];
$session = SimpleSAML_Session::getSessionFromRequest();
if (!$session->isValid($source)) {
SimpleSAML\Logger::warning(
'Received logout from an invalid authentication source '.
var_export($source, true)
);
return;
}
$session->doLogout($source);
}
/**
* Add a logout callback association.
*
* This function adds a logout callback association, which allows us to initiate
* a logout later based on the $assoc-value.
*
* Note that logout-associations exists per authentication source. A logout association
* from one authentication source cannot be called from a different authentication source.
*
* @param string $assoc The identifier for this logout association.
* @param array $state The state array passed to the authenticate-function.
*/
protected function addLogoutCallback($assoc, $state)
{
assert(is_string($assoc));
assert(is_array($state));
if (!array_key_exists('LogoutCallback', $state)) {
// the authentication requester doesn't have a logout callback
return;
}
$callback = $state['LogoutCallback'];
if (array_key_exists('LogoutCallbackState', $state)) {
$callbackState = $state['LogoutCallbackState'];
} else {
$callbackState = array();
}
$id = strlen($this->authId).':'.$this->authId.$assoc;
$data = array(
'callback' => $callback,
'state' => $callbackState,
);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData(
'SimpleSAML_Auth_Source.LogoutCallbacks',
$id,
$data,
SimpleSAML_Session::DATA_TIMEOUT_SESSION_END
);
}
/**
* Call a logout callback based on association.
*
* This function calls a logout callback based on an association saved with
* addLogoutCallback(...).
*
* This function always returns.
*
* @param string $assoc The logout association which should be called.
*/
protected function callLogoutCallback($assoc)
{
assert(is_string($assoc));
$id = strlen($this->authId).':'.$this->authId.$assoc;
$session = SimpleSAML_Session::getSessionFromRequest();
$data = $session->getData('SimpleSAML_Auth_Source.LogoutCallbacks', $id);
if ($data === null) {
// FIXME: fix for IdP-first flow (issue 397) -> reevaluate logout callback infrastructure
$session->doLogout($this->authId);
return;
}
assert(is_array($data));
assert(array_key_exists('callback', $data));
assert(array_key_exists('state', $data));
$callback = $data['callback'];
$callbackState = $data['state'];
$session->deleteData('SimpleSAML_Auth_Source.LogoutCallbacks', $id);
call_user_func($callback, $callbackState);
}
/**
* Retrieve list of authentication sources.
*
* @return array The id of all authentication sources.
*/
public static function getSources()
{
$config = SimpleSAML_Configuration::getOptionalConfig('authsources.php');
return $config->getOptions();
}
/**
* Make sure that the first element of an auth source is its identifier.
*
* @param array $source An array with the auth source configuration.
* @param string $id The auth source identifier.
*
* @throws Exception If the first element of $source is not an identifier for the auth source.
*/
protected static function validateSource($source, $id)
{
if (!array_key_exists(0, $source) || !is_string($source[0])) {
throw new Exception(
'Invalid authentication source \''.$id.
'\': First element must be a string which identifies the authentication source.'
);
}
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace SimpleSAML\Auth;
use SimpleSAML_Auth_Source;
interface SourceFactory
{
/**
* @param array $info
* @param array $config
* @return SimpleSAML_Auth_Source
*/
public function create(array $info, array $config);
}

420
lib/SimpleSAML/Auth/State.php Executable file
View File

@@ -0,0 +1,420 @@
<?php
/**
* This is a helper class for saving and loading state information.
*
* The state must be an associative array. This class will add additional keys to this
* array. These keys will always start with 'SimpleSAML_Auth_State.'.
*
* It is also possible to add a restart URL to the state. If state information is lost, for
* example because it timed out, or the user loaded a bookmarked page, the loadState function
* will redirect to this URL. To use this, set $state[SimpleSAML_Auth_State::RESTART] to this
* URL.
*
* Both the saveState and the loadState function takes in a $stage parameter. This parameter is
* a security feature, and is used to prevent the user from taking a state saved one place and
* using it as input a different place.
*
* The $stage parameter must be a unique string. To maintain uniqueness, it must be on the form
* "<classname>.<identifier>" or "<module>:<identifier>".
*
* There is also support for passing exceptions through the state.
* By defining an exception handler when creating the state array, users of the state
* array can call throwException with the state and the exception. This exception will
* be passed to the handler defined by the EXCEPTION_HANDLER_URL or EXCEPTION_HANDLER_FUNC
* elements of the state array.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Auth_State
{
/**
* The index in the state array which contains the identifier.
*/
const ID = 'SimpleSAML_Auth_State.id';
/**
* The index in the cloned state array which contains the identifier of the
* original state.
*/
const CLONE_ORIGINAL_ID = 'SimpleSAML_Auth_State.cloneOriginalId';
/**
* The index in the state array which contains the current stage.
*/
const STAGE = 'SimpleSAML_Auth_State.stage';
/**
* The index in the state array which contains the restart URL.
*/
const RESTART = 'SimpleSAML_Auth_State.restartURL';
/**
* The index in the state array which contains the exception handler URL.
*/
const EXCEPTION_HANDLER_URL = 'SimpleSAML_Auth_State.exceptionURL';
/**
* The index in the state array which contains the exception handler function.
*/
const EXCEPTION_HANDLER_FUNC = 'SimpleSAML_Auth_State.exceptionFunc';
/**
* The index in the state array which contains the exception data.
*/
const EXCEPTION_DATA = 'SimpleSAML_Auth_State.exceptionData';
/**
* The stage of a state with an exception.
*/
const EXCEPTION_STAGE = 'SimpleSAML_Auth_State.exceptionStage';
/**
* The URL parameter which contains the exception state id.
*/
const EXCEPTION_PARAM = 'SimpleSAML_Auth_State_exceptionId';
/**
* State timeout.
*/
private static $stateTimeout = null;
/**
* Get the persistent authentication state from the state array.
*
* @param array $state The state array to analyze.
*
* @return array The persistent authentication state.
*/
public static function getPersistentAuthData(array $state)
{
// save persistent authentication data
$persistent = array();
if (array_key_exists('PersistentAuthData', $state)) {
foreach ($state['PersistentAuthData'] as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
}
// add those that should always be included
$mandatory = array(
'Attributes',
'Expire',
'LogoutState',
'AuthInstant',
'RememberMe',
'saml:sp:NameID'
);
foreach ($mandatory as $key) {
if (isset($state[$key])) {
$persistent[$key] = $state[$key];
}
}
return $persistent;
}
/**
* Retrieve the ID of a state array.
*
* Note that this function will not save the state.
*
* @param array &$state The state array.
* @param bool $rawId Return a raw ID, without a restart URL. Defaults to FALSE.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function getStateId(&$state, $rawId = false)
{
assert(is_array($state));
assert(is_bool($rawId));
if (!array_key_exists(self::ID, $state)) {
$state[self::ID] = SimpleSAML\Utils\Random::generateID();
}
$id = $state[self::ID];
if ($rawId || !array_key_exists(self::RESTART, $state)) {
// Either raw ID or no restart URL. In any case, return the raw ID.
return $id;
}
// We have a restart URL. Return the ID with that URL.
return $id.':'.$state[self::RESTART];
}
/**
* Retrieve state timeout.
*
* @return integer State timeout.
*/
private static function getStateTimeout()
{
if (self::$stateTimeout === null) {
$globalConfig = SimpleSAML_Configuration::getInstance();
self::$stateTimeout = $globalConfig->getInteger('session.state.timeout', 60 * 60);
}
return self::$stateTimeout;
}
/**
* Save the state.
*
* This function saves the state, and returns an id which can be used to
* retrieve it later. It will also update the $state array with the identifier.
*
* @param array &$state The login request state.
* @param string $stage The current stage in the login process.
* @param bool $rawId Return a raw ID, without a restart URL.
*
* @return string Identifier which can be used to retrieve the state later.
*/
public static function saveState(&$state, $stage, $rawId = false)
{
assert(is_array($state));
assert(is_string($stage));
assert(is_bool($rawId));
$return = self::getStateId($state, $rawId);
$id = $state[self::ID];
// Save stage
$state[self::STAGE] = $stage;
// Save state
$serializedState = serialize($state);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData('SimpleSAML_Auth_State', $id, $serializedState, self::getStateTimeout());
SimpleSAML\Logger::debug('Saved state: '.var_export($return, true));
return $return;
}
/**
* Clone the state.
*
* This function clones and returns the new cloned state.
*
* @param array $state The original request state.
*
* @return array Cloned state data.
*/
public static function cloneState(array $state)
{
$clonedState = $state;
if (array_key_exists(self::ID, $state)) {
$clonedState[self::CLONE_ORIGINAL_ID] = $state[self::ID];
unset($clonedState[self::ID]);
SimpleSAML\Logger::debug('Cloned state: '.var_export($state[self::ID], true));
} else {
SimpleSAML\Logger::debug('Cloned state with undefined id.');
}
return $clonedState;
}
/**
* Retrieve saved state.
*
* This function retrieves saved state information. If the state information has been lost,
* it will attempt to restart the request by calling the restart URL which is embedded in the
* state information. If there is no restart information available, an exception will be thrown.
*
* @param string $id State identifier (with embedded restart information).
* @param string $stage The stage the state should have been saved in.
* @param bool $allowMissing Whether to allow the state to be missing.
*
* @throws SimpleSAML_Error_NoState If we couldn't find the state and there's no URL defined to redirect to.
* @throws Exception If the stage of the state is invalid and there's no URL defined to redirect to.
*
* @return array|NULL State information, or null if the state is missing and $allowMissing is true.
*/
public static function loadState($id, $stage, $allowMissing = false)
{
assert(is_string($id));
assert(is_string($stage));
assert(is_bool($allowMissing));
SimpleSAML\Logger::debug('Loading state: '.var_export($id, true));
$sid = self::parseStateID($id);
$session = SimpleSAML_Session::getSessionFromRequest();
$state = $session->getData('SimpleSAML_Auth_State', $sid['id']);
if ($state === null) {
// Could not find saved data
if ($allowMissing) {
return null;
}
if ($sid['url'] === null) {
throw new SimpleSAML_Error_NoState();
}
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']);
}
$state = unserialize($state);
assert(is_array($state));
assert(array_key_exists(self::ID, $state));
assert(array_key_exists(self::STAGE, $state));
// Verify stage
if ($state[self::STAGE] !== $stage) {
/* This could be a user trying to bypass security, but most likely it is just
* someone using the back-button in the browser. We try to restart the
* request if that is possible. If not, show an error.
*/
$msg = 'Wrong stage in state. Was \''.$state[self::STAGE].
'\', should be \''.$stage.'\'.';
SimpleSAML\Logger::warning($msg);
if ($sid['url'] === null) {
throw new Exception($msg);
}
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($sid['url']);
}
return $state;
}
/**
* Delete state.
*
* This function deletes the given state to prevent the user from reusing it later.
*
* @param array &$state The state which should be deleted.
*/
public static function deleteState(&$state)
{
assert(is_array($state));
if (!array_key_exists(self::ID, $state)) {
// This state hasn't been saved
return;
}
SimpleSAML\Logger::debug('Deleting state: '.var_export($state[self::ID], true));
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('SimpleSAML_Auth_State', $state[self::ID]);
}
/**
* Throw exception to the state exception handler.
*
* @param array $state The state array.
* @param SimpleSAML_Error_Exception $exception The exception.
*
* @throws SimpleSAML_Error_Exception If there is no exception handler defined, it will just throw the $exception.
*/
public static function throwException($state, SimpleSAML_Error_Exception $exception)
{
assert(is_array($state));
if (array_key_exists(self::EXCEPTION_HANDLER_URL, $state)) {
// Save the exception
$state[self::EXCEPTION_DATA] = $exception;
$id = self::saveState($state, self::EXCEPTION_STAGE);
// Redirect to the exception handler
\SimpleSAML\Utils\HTTP::redirectTrustedURL(
$state[self::EXCEPTION_HANDLER_URL],
array(self::EXCEPTION_PARAM => $id)
);
} elseif (array_key_exists(self::EXCEPTION_HANDLER_FUNC, $state)) {
// Call the exception handler
$func = $state[self::EXCEPTION_HANDLER_FUNC];
assert(is_callable($func));
call_user_func($func, $exception, $state);
assert(false);
} else {
/*
* No exception handler is defined for the current state.
*/
throw $exception;
}
}
/**
* Retrieve an exception state.
*
* @param string|NULL $id The exception id. Can be NULL, in which case it will be retrieved from the request.
*
* @return array|NULL The state array with the exception, or NULL if no exception was thrown.
*/
public static function loadExceptionState($id = null)
{
assert(is_string($id) || $id === null);
if ($id === null) {
if (!array_key_exists(self::EXCEPTION_PARAM, $_REQUEST)) {
// No exception
return null;
}
$id = $_REQUEST[self::EXCEPTION_PARAM];
}
$state = self::loadState($id, self::EXCEPTION_STAGE);
assert(array_key_exists(self::EXCEPTION_DATA, $state));
return $state;
}
/**
* Get the ID and (optionally) a URL embedded in a StateID, in the form 'id:url'.
*
* @param string $stateId The state ID to use.
*
* @return array A hashed array with the ID and the URL (if any), in the 'id' and 'url' keys, respectively. If
* there's no URL in the input parameter, NULL will be returned as the value for the 'url' key.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function parseStateID($stateId)
{
$tmp = explode(':', $stateId, 2);
$id = $tmp[0];
$url = null;
if (count($tmp) === 2) {
$url = $tmp[1];
}
return array('id' => $id, 'url' => $url);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace SimpleSAML\Auth;
/**
* A class that generates and verifies time-limited tokens.
*/
class TimeLimitedToken
{
/**
* @var string
*/
protected $secretSalt;
/**
* @var int
*/
protected $lifetime;
/**
* @var int
*/
protected $skew;
/**
* @var string
*/
protected $algo;
/**
* Create a new time-limited token.
*
* Please note that the default algorithm will change in SSP 1.15.0 to SHA-256 instead of SHA-1.
*
* @param int $lifetime Token lifetime in seconds. Defaults to 900 (15 min).
* @param string $secretSalt A random and unique salt per installation. Defaults to the salt in the configuration.
* @param int $skew The allowed time skew (in seconds) to correct clock deviations. Defaults to 1 second.
* @param string $algo The hash algorithm to use to generate the tokens. Defaults to SHA-1.
*
* @throws \InvalidArgumentException if the given parameters are invalid.
*/
public function __construct($lifetime = 900, $secretSalt = null, $skew = 1, $algo = 'sha1')
{
if ($secretSalt === null) {
$secretSalt = \SimpleSAML\Utils\Config::getSecretSalt();
}
if (!in_array($algo, hash_algos(), true)) {
throw new \InvalidArgumentException('Invalid hash algorithm "'.$algo.'"');
}
$this->secretSalt = $secretSalt;
$this->lifetime = $lifetime;
$this->skew = $skew;
$this->algo = $algo;
}
/**
* Add some given data to the current token. This data will be needed later too for token validation.
*
* This mechanism can be used to provide context for a token, such as a user identifier of the only subject
* authorised to use it. Note also that multiple data can be added to the token. This means that upon validation,
* not only the same data must be added, but also in the same order.
*
* @param string $data The data to incorporate into the current token.
*/
public function addVerificationData($data)
{
$this->secretSalt .= '|'.$data;
}
/**
* Calculates a token value for a given offset.
*
* @param int $offset The offset to use.
* @param int|null $time The time stamp to which the offset is relative to. Defaults to the current time.
*
* @return string The token for the given time and offset.
*/
private function calculateTokenValue($offset, $time = null)
{
if ($time === null) {
$time = time();
}
// a secret salt that should be randomly generated for each installation
return hash(
$this->algo,
$offset.':'.floor(($time - $offset) / ($this->lifetime + $this->skew)).':'.$this->secretSalt
);
}
/**
* Generates a token that contains an offset and a token value, using the current offset.
*
* @return string A time-limited token with the offset respect to the beginning of its time slot prepended.
*/
public function generate()
{
$time = time();
$current_offset = ($time - $this->skew) % ($this->lifetime + $this->skew);
return dechex($current_offset).'-'.$this->calculateTokenValue($current_offset, $time);
}
/**
* @see generate
* @deprecated This method will be removed in SSP 2.0. Use generate() instead.
*/
public function generate_token()
{
return $this->generate();
}
/**
* Validates a token by calculating the token value for the provided offset and comparing it.
*
* @param string $token The token to validate.
*
* @return boolean True if the given token is currently valid, false otherwise.
*/
public function validate($token)
{
$splittoken = explode('-', $token);
if (count($splittoken) !== 2) {
return false;
}
$offset = intval(hexdec($splittoken[0]));
$value = $splittoken[1];
return ($this->calculateTokenValue($offset) === $value);
}
/**
* @see validate
* @deprecated This method will be removed in SSP 2.0. Use validate() instead.
*/
public function validate_token($token)
{
return $this->validate($token);
}
}

166
lib/SimpleSAML/AuthMemCookie.php Executable file
View File

@@ -0,0 +1,166 @@
<?php
/**
* This is a helper class for the Auth MemCookie module.
* It handles the configuration, and implements the logout handler.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*
* @deprecated This class has been deprecated and will be removed in SSP 2.0. Use the memcookie module instead.
*/
class SimpleSAML_AuthMemCookie
{
/**
* @var SimpleSAML_AuthMemCookie This is the singleton instance of this class.
*/
private static $instance = null;
/**
* @var SimpleSAML_Configuration The configuration for Auth MemCookie.
*/
private $amcConfig;
/**
* This function is used to retrieve the singleton instance of this class.
*
* @return SimpleSAML_AuthMemCookie The singleton instance of this class.
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new SimpleSAML_AuthMemCookie();
}
return self::$instance;
}
/**
* This function implements the constructor for this class. It loads the Auth MemCookie configuration.
*/
private function __construct()
{
// load AuthMemCookie configuration
$this->amcConfig = SimpleSAML_Configuration::getConfig('authmemcookie.php');
}
/**
* Retrieve the authentication source that should be used to authenticate the user.
*
* @return string The login type which should be used for Auth MemCookie.
*/
public function getAuthSource()
{
return $this->amcConfig->getString('authsource');
}
/**
* This function retrieves the name of the cookie from the configuration.
*
* @return string The name of the cookie.
* @throws Exception If the value of the 'cookiename' configuration option is invalid.
*/
public function getCookieName()
{
$cookieName = $this->amcConfig->getString('cookiename', 'AuthMemCookie');
if (!is_string($cookieName) || strlen($cookieName) === 0) {
throw new Exception(
"Configuration option 'cookiename' contains an invalid value. This option should be a string."
);
}
return $cookieName;
}
/**
* This function retrieves the name of the attribute which contains the username from the configuration.
*
* @return string The name of the attribute which contains the username.
*/
public function getUsernameAttr()
{
$usernameAttr = $this->amcConfig->getString('username', null);
return $usernameAttr;
}
/**
* This function retrieves the name of the attribute which contains the groups from the configuration.
*
* @return string The name of the attribute which contains the groups.
*/
public function getGroupsAttr()
{
$groupsAttr = $this->amcConfig->getString('groups', null);
return $groupsAttr;
}
/**
* This function creates and initializes a Memcache object from our configuration.
*
* @return Memcache A Memcache object initialized from our configuration.
* @throws Exception If the servers configuration is invalid.
*/
public function getMemcache()
{
$memcacheHost = $this->amcConfig->getString('memcache.host', '127.0.0.1');
$memcachePort = $this->amcConfig->getInteger('memcache.port', 11211);
$class = class_exists('Memcache') ? 'Memcache' : (class_exists('Memcached') ? 'Memcached' : false);
if (!$class) {
throw new Exception('Missing Memcached implementation. You must install either the Memcache or Memcached extension.');
}
// Create the Memcache(d) object.
$memcache = new $class();
foreach (explode(',', $memcacheHost) as $memcacheHost) {
$memcache->addServer($memcacheHost, $memcachePort);
}
return $memcache;
}
/**
* This function logs the user out by deleting the session information from memcache.
*/
private function doLogout()
{
$cookieName = $this->getCookieName();
// check if we have a valid cookie
if (!array_key_exists($cookieName, $_COOKIE)) {
return;
}
$sessionID = $_COOKIE[$cookieName];
// delete the session from memcache
$memcache = $this->getMemcache();
$memcache->delete($sessionID);
// delete the session cookie
\SimpleSAML\Utils\HTTP::setCookie($cookieName, null);
}
/**
* This function implements the logout handler. It deletes the information from Memcache.
*/
public static function logoutHandler()
{
self::getInstance()->doLogout();
}
}

View File

@@ -0,0 +1,190 @@
<?php
/**
* Implementation of the Shibboleth 1.3 Artifact binding.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Bindings\Shib13;
use SAML2\DOMDocumentFactory;
use SimpleSAML\Utils\Config;
use SimpleSAML\Utils\HTTP;
use SimpleSAML\Utils\Random;
use SimpleSAML\Utils\System;
use SimpleSAML\Utils\Time;
use SimpleSAML\Utils\XML;
class Artifact
{
/**
* Parse the query string, and extract the SAMLart parameters.
*
* This function is required because each query contains multiple
* artifact with the same parameter name.
*
* @return array The artifacts.
*/
private static function getArtifacts()
{
assert(array_key_exists('QUERY_STRING', $_SERVER));
// We need to process the query string manually, to capture all SAMLart parameters
$artifacts = array();
$elements = explode('&', $_SERVER['QUERY_STRING']);
foreach ($elements as $element) {
list($name, $value) = explode('=', $element, 2);
$name = urldecode($name);
$value = urldecode($value);
if ($name === 'SAMLart') {
$artifacts[] = $value;
}
}
return $artifacts;
}
/**
* Build the request we will send to the IdP.
*
* @param array $artifacts The artifacts we will request.
* @return string The request, as an XML string.
*/
private static function buildRequest(array $artifacts)
{
$msg = '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">' .
'<SOAP-ENV:Body>' .
'<samlp:Request xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol"' .
' RequestID="' . Random::generateID() . '"' .
' MajorVersion="1" MinorVersion="1"' .
' IssueInstant="' . Time::generateTimestamp() . '"' .
'>';
foreach ($artifacts as $a) {
$msg .= '<samlp:AssertionArtifact>' . htmlspecialchars($a) . '</samlp:AssertionArtifact>';
}
$msg .= '</samlp:Request>' .
'</SOAP-ENV:Body>' .
'</SOAP-ENV:Envelope>';
return $msg;
}
/**
* Extract the response element from the SOAP response.
*
* @param string $soapResponse The SOAP response.
* @return string The <saml1p:Response> element, as a string.
* @throws \SimpleSAML_Error_Exception
*/
private static function extractResponse($soapResponse)
{
assert(is_string($soapResponse));
try {
$doc = DOMDocumentFactory::fromString($soapResponse);
} catch (\Exception $e) {
throw new \SimpleSAML_Error_Exception('Error parsing SAML 1 artifact response.');
}
$soapEnvelope = $doc->firstChild;
if (!XML::isDOMNodeOfType($soapEnvelope, 'Envelope', 'http://schemas.xmlsoap.org/soap/envelope/')) {
throw new \SimpleSAML_Error_Exception('Expected artifact response to contain a <soap:Envelope> element.');
}
$soapBody = XML::getDOMChildren($soapEnvelope, 'Body', 'http://schemas.xmlsoap.org/soap/envelope/');
if (count($soapBody) === 0) {
throw new \SimpleSAML_Error_Exception('Couldn\'t find <soap:Body> in <soap:Envelope>.');
}
$soapBody = $soapBody[0];
$responseElement = XML::getDOMChildren($soapBody, 'Response', 'urn:oasis:names:tc:SAML:1.0:protocol');
if (count($responseElement) === 0) {
throw new \SimpleSAML_Error_Exception('Couldn\'t find <saml1p:Response> in <soap:Body>.');
}
$responseElement = $responseElement[0];
/*
* Save the <saml1p:Response> element. Note that we need to import it
* into a new document, in order to preserve namespace declarations.
*/
$newDoc = DOMDocumentFactory::create();
$newDoc->appendChild($newDoc->importNode($responseElement, true));
$responseXML = $newDoc->saveXML();
return $responseXML;
}
/**
* This function receives a SAML 1.1 artifact.
*
* @param \SimpleSAML_Configuration $spMetadata The metadata of the SP.
* @param \SimpleSAML_Configuration $idpMetadata The metadata of the IdP.
* @return string The <saml1p:Response> element, as an XML string.
* @throws \SimpleSAML_Error_Exception
*/
public static function receive(\SimpleSAML_Configuration $spMetadata, \SimpleSAML_Configuration $idpMetadata)
{
$artifacts = self::getArtifacts();
$request = self::buildRequest($artifacts);
XML::debugSAMLMessage($request, 'out');
$url = $idpMetadata->getDefaultEndpoint('ArtifactResolutionService', array('urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding'));
$url = $url['Location'];
$peerPublicKeys = $idpMetadata->getPublicKeys('signing', true);
$certData = '';
foreach ($peerPublicKeys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
$certData .= "-----BEGIN CERTIFICATE-----\n" .
chunk_split($key['X509Certificate'], 64) .
"-----END CERTIFICATE-----\n";
}
$file = System::getTempDir() . DIRECTORY_SEPARATOR . sha1($certData) . '.crt';
if (!file_exists($file)) {
System::writeFile($file, $certData);
}
$spKeyCertFile = Config::getCertPath($spMetadata->getString('privatekey'));
$opts = array(
'ssl' => array(
'verify_peer' => true,
'cafile' => $file,
'local_cert' => $spKeyCertFile,
'capture_peer_cert' => true,
'capture_peer_chain' => true,
),
'http' => array(
'method' => 'POST',
'content' => $request,
'header' => 'SOAPAction: http://www.oasis-open.org/committees/security' . "\r\n" .
'Content-Type: text/xml',
),
);
// Fetch the artifact
$response = HTTP::fetch($url, $opts);
/** @var string $response */
XML::debugSAMLMessage($response, 'in');
// Find the response in the SOAP message
$response = self::extractResponse($response);
return $response;
}
}

View File

@@ -0,0 +1,154 @@
<?php
/**
* Implementation of the Shibboleth 1.3 HTTP-POST binding.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Bindings\Shib13;
use SAML2\DOMDocumentFactory;
use SimpleSAML\Utils\Crypto;
use SimpleSAML\Utils\HTTP;
use SimpleSAML\Utils\XML;
use SimpleSAML\XML\Shib13\AuthnResponse;
use SimpleSAML\XML\Signer;
class HTTPPost
{
/**
* @var \SimpleSAML_Configuration
*/
private $configuration = null;
/**
* @var \SimpleSAML_Metadata_MetaDataStorageHandler
*/
private $metadata = null;
/**
* Constructor for the \SimpleSAML\Bindings\Shib13\HTTPPost class.
*
* @param \SimpleSAML_Configuration $configuration The configuration to use.
* @param \SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore A store where to find metadata.
*/
public function __construct(
\SimpleSAML_Configuration $configuration,
\SimpleSAML_Metadata_MetaDataStorageHandler $metadatastore
) {
$this->configuration = $configuration;
$this->metadata = $metadatastore;
}
/**
* Send an authenticationResponse using HTTP-POST.
*
* @param string $response The response which should be sent.
* @param \SimpleSAML_Configuration $idpmd The metadata of the IdP which is sending the response.
* @param \SimpleSAML_Configuration $spmd The metadata of the SP which is receiving the response.
* @param string|null $relayState The relaystate for the SP.
* @param string $shire The shire which should receive the response.
*/
public function sendResponse(
$response,
\SimpleSAML_Configuration $idpmd,
\SimpleSAML_Configuration $spmd,
$relayState,
$shire
) {
XML::checkSAMLMessage($response, 'saml11');
$privatekey = Crypto::loadPrivateKey($idpmd, true);
$publickey = Crypto::loadPublicKey($idpmd, true);
$responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response));
$responseroot = $responsedom->getElementsByTagName('Response')->item(0);
$firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0);
/* Determine what we should sign - either the Response element or the Assertion. The default is to sign the
* Assertion, but that can be overridden by the 'signresponse' option in the SP metadata or
* 'saml20.signresponse' in the global configuration.
*
* TODO: neither 'signresponse' nor 'shib13.signresponse' are valid options any longer. Remove!
*/
if ($spmd->hasValue('signresponse')) {
$signResponse = $spmd->getBoolean('signresponse');
} else {
$signResponse = $this->configuration->getBoolean('shib13.signresponse', true);
}
// check if we have an assertion to sign. Force to sign the response if not
if ($firstassertionroot === null) {
$signResponse = true;
}
$signer = new Signer(array(
'privatekey_array' => $privatekey,
'publickey_array' => $publickey,
'id' => ($signResponse ? 'ResponseID' : 'AssertionID'),
));
if ($idpmd->hasValue('certificatechain')) {
$signer->addCertificate($idpmd->getString('certificatechain'));
}
if ($signResponse) {
// sign the response - this must be done after encrypting the assertion
// we insert the signature before the saml2p:Status element
$statusElements = XML::getDOMChildren($responseroot, 'Status', '@saml1p');
assert(count($statusElements) === 1);
$signer->sign($responseroot, $responseroot, $statusElements[0]);
} else {
// Sign the assertion
$signer->sign($firstassertionroot, $firstassertionroot);
}
$response = $responsedom->saveXML();
XML::debugSAMLMessage($response, 'out');
HTTP::submitPOSTData($shire, array(
'TARGET' => $relayState,
'SAMLResponse' => base64_encode($response),
));
}
/**
* Decode a received response.
*
* @param array $post POST data received.
* @return \SimpleSAML\XML\Shib13\AuthnResponse The response decoded into an object.
* @throws \Exception If there is no SAMLResponse parameter.
*/
public function decodeResponse($post)
{
assert(is_array($post));
if (!array_key_exists('SAMLResponse', $post)) {
throw new \Exception('Missing required SAMLResponse parameter.');
}
$rawResponse = $post['SAMLResponse'];
$samlResponseXML = base64_decode($rawResponse);
XML::debugSAMLMessage($samlResponseXML, 'in');
XML::checkSAMLMessage($samlResponseXML, 'saml11');
$samlResponse = new AuthnResponse();
$samlResponse->setXML($samlResponseXML);
if (array_key_exists('TARGET', $post)) {
$samlResponse->setRelayState($post['TARGET']);
}
return $samlResponse;
}
}

1392
lib/SimpleSAML/Configuration.php Executable file

File diff suppressed because it is too large Load Diff

293
lib/SimpleSAML/Database.php Executable file
View File

@@ -0,0 +1,293 @@
<?php
namespace SimpleSAML;
/**
* This file implements functions to read and write to a group of database servers.
*
* This database class supports a single database, or a master/slave configuration with as many defined slaves as a
* user would like.
*
* The goal of this class is to provide a single mechanism to connect to a database that can be reused by any component
* within SimpleSAMLphp including modules. When using this class, the global configuration should be passed here, but in
* the case of a module that has a good reason to use a different database, such as sqlauth, an alternative config file
* can be provided.
*
* @author Tyler Antonio, University of Alberta. <tantonio@ualberta.ca>
* @package SimpleSAMLphp
*/
class Database
{
/**
* This variable holds the instance of the session - Singleton approach.
*/
private static $instance = array();
/**
* PDO Object for the Master database server
*/
private $dbMaster;
/**
* Array of PDO Objects for configured database slaves
*/
private $dbSlaves = array();
/**
* Prefix to apply to the tables
*/
private $tablePrefix;
/**
* Array with information on the last error occurred.
*/
private $lastError;
/**
* Retrieves the current database instance. Will create a new one if there isn't an existing connection.
*
* @param \SimpleSAML_Configuration $altConfig Optional: Instance of a SimpleSAML_Configuration class
*
* @return \SimpleSAML\Database The shared database connection.
*/
public static function getInstance($altConfig = null)
{
$config = ($altConfig) ? $altConfig : \SimpleSAML_Configuration::getInstance();
$instanceId = self::generateInstanceId($config);
// check if we already have initialized the session
if (isset(self::$instance[$instanceId])) {
return self::$instance[$instanceId];
}
// create a new session
self::$instance[$instanceId] = new Database($config);
return self::$instance[$instanceId];
}
/**
* Private constructor that restricts instantiation to getInstance().
*
* @param \SimpleSAML_Configuration $config Instance of the SimpleSAML_Configuration class
*/
private function __construct($config)
{
$driverOptions = $config->getArray('database.driver_options', array());
if ($config->getBoolean('database.persistent', true)) {
$driverOptions = array(\PDO::ATTR_PERSISTENT => true);
}
// connect to the master
$this->dbMaster = $this->connect(
$config->getString('database.dsn'),
$config->getString('database.username', null),
$config->getString('database.password', null),
$driverOptions
);
// connect to any configured slaves
$slaves = $config->getArray('database.slaves', array());
foreach ($slaves as $slave) {
array_push(
$this->dbSlaves,
$this->connect(
$slave['dsn'],
$slave['username'],
$slave['password'],
$driverOptions
)
);
}
$this->tablePrefix = $config->getString('database.prefix', '');
}
/**
* Generate an Instance ID based on the database configuration.
*
* @param \SimpleSAML_Configuration $config Configuration class
*
* @return string $instanceId
*/
private static function generateInstanceId($config)
{
$assembledConfig = array(
'master' => array(
'database.dsn' => $config->getString('database.dsn'),
'database.username' => $config->getString('database.username', null),
'database.password' => $config->getString('database.password', null),
'database.prefix' => $config->getString('database.prefix', ''),
'database.persistent' => $config->getBoolean('database.persistent', false),
),
'slaves' => $config->getArray('database.slaves', array()),
);
return sha1(serialize($assembledConfig));
}
/**
* This function connects to a database.
*
* @param string $dsn Database connection string
* @param string $username SQL user
* @param string $password SQL password
* @param array $options PDO options
*
* @throws \Exception If an error happens while trying to connect to the database.
* @return \PDO object
*/
private function connect($dsn, $username, $password, $options)
{
try {
$db = new \PDO($dsn, $username, $password, $options);
$db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return $db;
} catch (\PDOException $e) {
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This function randomly selects a slave database server to query. In the event no slaves are configured, it will
* return the master.
*
* @return \PDO object
*/
private function getSlave()
{
if (count($this->dbSlaves) > 0) {
$slaveId = rand(0, count($this->dbSlaves) - 1);
return $this->dbSlaves[$slaveId];
} else {
return $this->dbMaster;
}
}
/**
* This function simply applies the table prefix to a supplied table name.
*
* @param string $table Table to apply prefix to, if configured
*
* @return string Table with configured prefix
*/
public function applyPrefix($table)
{
return $this->tablePrefix.$table;
}
/**
* This function queries the database
*
* @param \PDO $db PDO object to use
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @throws \Exception If an error happens while trying to execute the query.
* @return \PDOStatement object
*/
private function query($db, $stmt, $params)
{
assert(is_object($db));
assert(is_string($stmt));
assert(is_array($params));
try {
$query = $db->prepare($stmt);
foreach ($params as $param => $value) {
if (is_array($value)) {
$query->bindValue(":$param", $value[0], ($value[1]) ? $value[1] : \PDO::PARAM_STR);
} else {
$query->bindValue(":$param", $value, \PDO::PARAM_STR);
}
}
$query->execute();
return $query;
} catch (\PDOException $e) {
$this->lastError = $db->errorInfo();
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This function queries the database without using a prepared statement.
*
* @param \PDO $db PDO object to use
* @param string $stmt An SQL statement to execute, previously escaped.
*
* @throws \Exception If an error happens while trying to execute the query.
* @return int The number of rows affected.
*/
private function exec($db, $stmt)
{
assert(is_object($db));
assert(is_string($stmt));
try {
return $db->exec($stmt);
} catch (\PDOException $e) {
$this->lastError = $db->errorInfo();
throw new \Exception("Database error: ".$e->getMessage());
}
}
/**
* This executes queries directly on the master.
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return int The number of rows affected by the query.
*/
public function write($stmt, $params = array())
{
$db = $this->dbMaster;
if (is_array($params)) {
$obj = $this->query($db, $stmt, $params);
return $obj->rowCount();
} else {
return $this->exec($db, $stmt);
}
}
/**
* This executes queries on a database server that is determined by this::getSlave().
*
* @param string $stmt Prepared SQL statement
* @param array $params Parameters
*
* @return \PDOStatement object
*/
public function read($stmt, $params = array())
{
$db = $this->getSlave();
return $this->query($db, $stmt, $params);
}
/**
* Return an array with information about the last operation performed in the database.
*
* @return array The array with error information.
*/
public function getLastError()
{
return $this->lastError;
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Class for creating exceptions from assertion failures.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Assertion extends SimpleSAML_Error_Exception
{
/**
* The assertion which failed, or null if only an expression was passed to the
* assert-function.
*/
private $assertion;
/**
* Constructor for the assertion exception.
*
* Should only be called from the onAssertion handler.
*
* @param string|null $assertion The assertion which failed, or null if the assert-function was
* given an expression.
*/
public function __construct($assertion = null)
{
assert($assertion === null || is_string($assertion));
$msg = 'Assertion failed: ' . var_export($assertion, true);
parent::__construct($msg);
$this->assertion = $assertion;
}
/**
* Retrieve the assertion which failed.
*
* @return string|null The assertion which failed, or null if the assert-function was called with an expression.
*/
public function getAssertion()
{
return $this->assertion;
}
/**
* Install this assertion handler.
*
* This function will register this assertion handler. If will not enable assertions if they are
* disabled.
*/
public static function installHandler()
{
assert_options(ASSERT_WARNING, 0);
assert_options(ASSERT_QUIET_EVAL, 0);
assert_options(ASSERT_CALLBACK, array('SimpleSAML_Error_Assertion', 'onAssertion'));
}
/**
* Handle assertion.
*
* This function handles an assertion.
*
* @param string $file The file assert was called from.
* @param int $line The line assert was called from.
* @param mixed $message The expression which was passed to the assert-function.
*/
public static function onAssertion($file, $line, $message)
{
if (!empty($message)) {
$exception = new self($message);
} else {
$exception = new self();
}
$exception->logError();
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* Baseclass for auth source exceptions.
*
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_AuthSource extends SimpleSAML_Error_Error
{
/**
* Authsource module name.
*/
private $authsource;
/**
* Reason why this request was invalid.
*/
private $reason;
/**
* Create a new AuthSource error.
*
* @param string $authsource Authsource module name from where this error was thrown.
* @param string $reason Description of the error.
*/
public function __construct($authsource, $reason, $cause = null)
{
assert(is_string($authsource));
assert(is_string($reason));
$this->authsource = $authsource;
$this->reason = $reason;
parent::__construct(
array(
'AUTHSOURCEERROR',
'%AUTHSOURCE%' => htmlspecialchars(var_export($this->authsource, true)),
'%REASON%' => htmlspecialchars(var_export($this->reason, true))
),
$cause
);
$this->message = "Error with authentication source '$authsource': $reason";
}
/**
* Retrieve the authsource module name from where this error was thrown.
*
* @return string Authsource module name.
*/
public function getAuthSource()
{
return $this->authsource;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Exception which will show a 400 Bad Request error page.
*
* This exception can be thrown from within an module page handler. The user will then be
* shown a 400 Bad Request error page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_BadRequest extends SimpleSAML_Error_Error
{
/**
* Reason why this request was invalid.
*/
private $reason;
/**
* Create a new BadRequest error.
*
* @param string $reason Description of why the request was unacceptable.
*/
public function __construct($reason)
{
assert(is_string($reason));
$this->reason = $reason;
parent::__construct(array('BADREQUEST', '%REASON%' => $this->reason));
$this->httpCode = 400;
}
/**
* Retrieve the reason why the request was invalid.
*
* @return string The reason why the request was invalid.
*/
public function getReason()
{
return $this->reason;
}
}

View File

@@ -0,0 +1,12 @@
<?php
/**
* Exception indicating illegal innput from user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_BadUserInput extends SimpleSAML_Error_User
{
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Exception to indicate that we cannot set a cookie.
*
* @author Jaime Pérez Crespo <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class CannotSetCookie extends \SimpleSAML_Error_Exception
{
/**
* The exception was thrown for unknown reasons.
*
* @var int
*/
const UNKNOWN = 0;
/**
* The exception was due to the HTTP headers being already sent, and therefore we cannot send additional headers to
* set the cookie.
*
* @var int
*/
const HEADERS_SENT = 1;
/**
* The exception was due to trying to set a secure cookie over an insecure channel.
*
* @var int
*/
const SECURE_COOKIE = 2;
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* This exception represents a configuration error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class ConfigurationError extends \SimpleSAML_Error_Error
{
/**
* The reason for this exception.
*
* @var null|string
*/
protected $reason;
/**
* The configuration file that caused this exception.
*
* @var null|string
*/
protected $config_file;
/**
* ConfigurationError constructor.
*
* @param string|null $reason The reason for this exception.
* @param string|null $file The configuration file that originated this error.
* @param array|null $config The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, array $config = null)
{
$file_str = '';
$reason_str = '.';
$params = array('CONFIG');
if ($file !== null) {
$params['%FILE%'] = $file;
$basepath = dirname(dirname(dirname(dirname(__FILE__)))).'/';
$file_str = '('.str_replace($basepath, '', $file).') ';
}
if ($reason !== null) {
$params['%REASON%'] = $reason;
$reason_str = ': '.$reason;
}
$this->reason = $reason;
$this->config_file = $file;
parent::__construct($params);
$this->message = 'The configuration '.$file_str.'is invalid'.$reason_str;
}
/**
* Get the reason for this exception.
*
* @return null|string The reason for this exception.
*/
public function getReason()
{
return $this->reason;
}
/**
* Get the configuration file that caused this exception.
*
* @return null|string The configuration file that caused this exception.
*/
public function getConfFile()
{
return $this->config_file;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* This exception represents a configuration error that we cannot recover from.
*
* Throwing a critical configuration error indicates that the configuration available is not usable, and as such
* SimpleSAMLphp should not try to use it. However, in certain situations we might find a specific configuration
* error that makes part of the configuration unusable, while the rest we can still use. In those cases, we can
* just pass a configuration array to the constructor, making sure the offending configuration options are removed,
* reset to defaults or guessed to some usable value.
*
* If, for example, we have an error in the 'baseurlpath' configuration option, we can still load the configuration
* and substitute the value of that option with one guessed from the environment, using
* \SimpleSAML\Utils\HTTP::guessPath(). Doing so, the error is still critical, but at least we can recover up to a
* certain point and inform about the error in an ordered manner, without blank pages, logs out of place or even
* segfaults.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class CriticalConfigurationError extends ConfigurationError
{
/**
* This is the bare minimum configuration that we can use.
*
* @var array
*/
private static $minimum_config = array(
'logging.handler' => 'errorlog',
'logging.level' => \SimpleSAML\Logger::DEBUG,
'errorreporting' => false,
'debug' => true,
);
/**
* CriticalConfigurationError constructor.
*
* @param string|null $reason The reason for this critical error.
* @param string|null $file The configuration file that originated this error.
* @param array|null The configuration array that led to this problem.
*/
public function __construct($reason = null, $file = null, $config = null)
{
if ($config === null) {
$config = self::$minimum_config;
$config['baseurlpath'] = \SimpleSAML\Utils\HTTP::guessBasePath();
}
\SimpleSAML_Configuration::loadFromArray(
$config,
'',
'simplesaml'
);
parent::__construct($reason, $file);
}
/**
* @param \Exception $exception
*
* @return CriticalConfigurationError
*/
public static function fromException(\Exception $exception)
{
$reason = null;
$file = null;
if ($exception instanceof ConfigurationError) {
$reason = $exception->getReason();
$file = $exception->getConfFile();
} else {
$reason = $exception->getMessage();
}
return new CriticalConfigurationError($reason, $file);
}
}

296
lib/SimpleSAML/Error/Error.php Executable file
View File

@@ -0,0 +1,296 @@
<?php
/**
* Class that wraps SimpleSAMLphp errors in exceptions.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Error extends SimpleSAML_Error_Exception
{
/**
* The error code.
*
* @var string
*/
private $errorCode;
/**
* The http code.
*
* @var integer
*/
protected $httpCode = 500;
/**
* The error title tag in dictionary.
*
* @var string
*/
private $dictTitle;
/**
* The error description tag in dictionary.
*
* @var string
*/
private $dictDescr;
/**
* The name of module that threw the error.
*
* @var string|null
*/
private $module = null;
/**
* The parameters for the error.
*
* @var array
*/
private $parameters;
/**
* Name of custom include template for the error.
*
* @var string|null
*/
protected $includeTemplate = null;
/**
* Constructor for this error.
*
* The error can either be given as a string, or as an array. If it is an array, the first element in the array
* (with index 0), is the error code, while the other elements are replacements for the error text.
*
* @param mixed $errorCode One of the error codes defined in the errors dictionary.
* @param Exception $cause The exception which caused this fatal error (if any). Optional.
* @param int|null $httpCode The HTTP response code to use. Optional.
*/
public function __construct($errorCode, Exception $cause = null, $httpCode = null)
{
assert(is_string($errorCode) || is_array($errorCode));
if (is_array($errorCode)) {
$this->parameters = $errorCode;
unset($this->parameters[0]);
$this->errorCode = $errorCode[0];
} else {
$this->parameters = array();
$this->errorCode = $errorCode;
}
if (isset($httpCode)) {
$this->httpCode = $httpCode;
}
$moduleCode = explode(':', $this->errorCode, 2);
if (count($moduleCode) === 2) {
$this->module = $moduleCode[0];
$this->dictTitle = '{'.$this->module.':errors:title_'.$moduleCode[1].'}';
$this->dictDescr = '{'.$this->module.':errors:descr_'.$moduleCode[1].'}';
} else {
$this->dictTitle = SimpleSAML\Error\ErrorCodes::getErrorCodeTitle($this->errorCode);
$this->dictDescr = SimpleSAML\Error\ErrorCodes::getErrorCodeDescription($this->errorCode);
}
if (!empty($this->parameters)) {
$msg = $this->errorCode.'(';
foreach ($this->parameters as $k => $v) {
if ($k === 0) {
continue;
}
$msg .= var_export($k, true).' => '.var_export($v, true).', ';
}
$msg = substr($msg, 0, -2).')';
} else {
$msg = $this->errorCode;
}
parent::__construct($msg, -1, $cause);
}
/**
* Retrieve the error code given when throwing this error.
*
* @return string The error code.
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Retrieve the error parameters given when throwing this error.
*
* @return array The parameters.
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Retrieve the error title tag in dictionary.
*
* @return string The error title tag.
*/
public function getDictTitle()
{
return $this->dictTitle;
}
/**
* Retrieve the error description tag in dictionary.
*
* @return string The error description tag.
*/
public function getDictDescr()
{
return $this->dictDescr;
}
/**
* Set the HTTP return code for this error.
*
* This should be overridden by subclasses who want a different return code than 500 Internal Server Error.
*/
protected function setHTTPCode()
{
// Some mostly used HTTP codes
$httpCodesMap = array(
400 => 'HTTP/1.0 400 Bad Request',
403 => 'HTTP/1.0 403 Forbidden',
404 => 'HTTP/1.0 404 Not Found',
405 => 'HTTP/1.0 405 Method Not Allowed',
500 => 'HTTP/1.0 500 Internal Server Error',
501 => 'HTTP/1.0 501 Method Not Implemented',
503 => 'HTTP/1.0 503 Service Temporarily Unavailable',
);
$httpCode = $this->httpCode;
if (function_exists('http_response_code')) {
http_response_code($httpCode);
return;
}
if (!array_key_exists($this->httpCode, $httpCodesMap)) {
$httpCode = 500;
SimpleSAML\Logger::warning('HTTP response code not defined: '.var_export($this->httpCode, true));
}
header($httpCodesMap[$httpCode]);
}
/**
* Save an error report.
*
* @return array The array with the error report data.
*/
protected function saveError()
{
$data = $this->format(true);
$emsg = array_shift($data);
$etrace = implode("\n", $data);
$reportId = bin2hex(openssl_random_pseudo_bytes(4));
SimpleSAML\Logger::error('Error report with id '.$reportId.' generated.');
$config = SimpleSAML_Configuration::getInstance();
$session = SimpleSAML_Session::getSessionFromRequest();
if (isset($_SERVER['HTTP_REFERER'])) {
$referer = $_SERVER['HTTP_REFERER'];
// remove anything after the first '?' or ';', just in case it contains any sensitive data
$referer = explode('?', $referer, 2);
$referer = $referer[0];
$referer = explode(';', $referer, 2);
$referer = $referer[0];
} else {
$referer = 'unknown';
}
$errorData = array(
'exceptionMsg' => $emsg,
'exceptionTrace' => $etrace,
'reportId' => $reportId,
'trackId' => $session->getTrackID(),
'url' => \SimpleSAML\Utils\HTTP::getSelfURLNoQuery(),
'version' => $config->getVersion(),
'referer' => $referer,
);
$session->setData('core:errorreport', $reportId, $errorData);
return $errorData;
}
/**
* Display this error.
*
* This method displays a standard SimpleSAMLphp error page and exits.
*/
public function show()
{
$this->setHTTPCode();
// log the error message
$this->logError();
$errorData = $this->saveError();
$config = SimpleSAML_Configuration::getInstance();
$data = array();
$data['showerrors'] = $config->getBoolean('showerrors', true);
$data['error'] = $errorData;
$data['errorCode'] = $this->errorCode;
$data['parameters'] = $this->parameters;
$data['module'] = $this->module;
$data['dictTitle'] = $this->dictTitle;
$data['dictDescr'] = $this->dictDescr;
$data['includeTemplate'] = $this->includeTemplate;
$data['clipboard.js'] = true;
// check if there is a valid technical contact email address
if ($config->getBoolean('errorreporting', true) &&
$config->getString('technicalcontact_email', 'na@example.org') !== 'na@example.org'
) {
// enable error reporting
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
$data['errorReportAddress'] = $baseurl.'errorreport.php';
}
$data['email'] = '';
$session = SimpleSAML_Session::getSessionFromRequest();
$authorities = $session->getAuthorities();
foreach ($authorities as $authority) {
$attributes = $session->getAuthData($authority, 'Attributes');
if ($attributes !== null && array_key_exists('mail', $attributes) && count($attributes['mail']) > 0) {
$data['email'] = $attributes['mail'][0];
break; // enough, don't need to get all available mails, if more than one
}
}
$show_function = $config->getArray('errors.show_function', null);
if (isset($show_function)) {
assert(is_callable($show_function));
call_user_func($show_function, $config, $data);
assert(false);
} else {
$t = new SimpleSAML_XHTML_Template($config, 'error.php', 'errors');
$t->data = array_merge($t->data, $data);
$t->data['dictTitleTranslated'] = $t->getTranslator()->t($t->data['dictTitle']);
$t->data['dictDescrTranslated'] = $t->getTranslator()->t($t->data['dictDescr'], $t->data['parameters']);
$t->show();
}
exit;
}
}

View File

@@ -0,0 +1,188 @@
<?php
/**
* Class that maps SimpleSAMLphp error codes to translateable strings.
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Error;
class ErrorCodes
{
/**
* Fetch all default translation strings for error code titles.
*
* @return array A map from error code to error code title
*/
final public static function defaultGetAllErrorCodeTitles()
{
return array(
'ACSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_ACSPARAMS}'),
'ARSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_ARSPARAMS}'),
'AUTHSOURCEERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_AUTHSOURCEERROR}'),
'BADREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_BADREQUEST}'),
'CASERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_CASERROR}'),
'CONFIG' => \SimpleSAML\Locale\Translate::noop('{errors:title_CONFIG}'),
'CREATEREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_CREATEREQUEST}'),
'DISCOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => \SimpleSAML\Locale\Translate::noop('{errors:title_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_INVALIDCERT}'),
'LDAPERROR' => \SimpleSAML\Locale\Translate::noop('{errors:title_LDAPERROR}'),
'LOGOUTINFOLOST' => \SimpleSAML\Locale\Translate::noop('{errors:title_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => \SimpleSAML\Locale\Translate::noop('{errors:title_MEMCACHEDOWN}'),
'METADATA' => \SimpleSAML\Locale\Translate::noop('{errors:title_METADATA}'),
'METADATANOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:title_METADATANOTFOUND}'),
'NOACCESS' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOACCESS}'),
'NOCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOCERT}'),
'NORELAYSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:title_NORELAYSTATE}'),
'NOSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOSTATE}'),
'NOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTFOUND}'),
'NOTFOUNDREASON' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTFOUNDREASON}'),
'NOTSET' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTSET}'),
'NOTVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_NOTVALIDCERT}'),
'PROCESSASSERTION' => \SimpleSAML\Locale\Translate::noop('{errors:title_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:title_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => \SimpleSAML\Locale\Translate::noop('{errors:title_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_SLOSERVICEPARAMS}'),
'SSOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:title_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => \SimpleSAML\Locale\Translate::noop('{errors:title_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => \SimpleSAML\Locale\Translate::noop('{errors:title_UNKNOWNCERT}'),
'USERABORTED' => \SimpleSAML\Locale\Translate::noop('{errors:title_USERABORTED}'),
'WRONGUSERPASS' => \SimpleSAML\Locale\Translate::noop('{errors:title_WRONGUSERPASS}'),
);
}
/**
* Fetch all translation strings for error code titles.
*
* Extend this to add error codes.
*
* @return array A map from error code to error code title
*/
public static function getAllErrorCodeTitles()
{
return self::defaultGetAllErrorCodeTitles();
}
/**
* Fetch all default translation strings for error code descriptions.
*
* @return string A map from error code to error code description
*/
final public static function defaultGetAllErrorCodeDescriptions()
{
return array(
'ACSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_ACSPARAMS}'),
'ARSPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_ARSPARAMS}'),
'AUTHSOURCEERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_AUTHSOURCEERROR}'),
'BADREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_BADREQUEST}'),
'CASERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CASERROR}'),
'CONFIG' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CONFIG}'),
'CREATEREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_CREATEREQUEST}'),
'DISCOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_DISCOPARAMS}'),
'GENERATEAUTHNRESPONSE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_GENERATEAUTHNRESPONSE}'),
'INVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_INVALIDCERT}'),
'LDAPERROR' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LDAPERROR}'),
'LOGOUTINFOLOST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LOGOUTINFOLOST}'),
'LOGOUTREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_LOGOUTREQUEST}'),
'MEMCACHEDOWN' => \SimpleSAML\Locale\Translate::noop('{errors:descr_MEMCACHEDOWN}'),
'METADATA' => \SimpleSAML\Locale\Translate::noop('{errors:descr_METADATA}'),
'METADATANOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:descr_METADATANOTFOUND}'),
'NOACCESS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOACCESS}'),
'NOCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOCERT}'),
'NORELAYSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NORELAYSTATE}'),
'NOSTATE' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOSTATE}'),
'NOTFOUND' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTFOUND}'),
'NOTFOUNDREASON' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTFOUNDREASON}'),
'NOTSET' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTSET}'),
'NOTVALIDCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_NOTVALIDCERT}'),
'PROCESSASSERTION' => \SimpleSAML\Locale\Translate::noop('{errors:descr_PROCESSASSERTION}'),
'PROCESSAUTHNREQUEST' => \SimpleSAML\Locale\Translate::noop('{errors:descr_PROCESSAUTHNREQUEST}'),
'RESPONSESTATUSNOSUCCESS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_RESPONSESTATUSNOSUCCESS}'),
'SLOSERVICEPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_SLOSERVICEPARAMS}'),
'SSOPARAMS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_SSOPARAMS}'),
'UNHANDLEDEXCEPTION' => \SimpleSAML\Locale\Translate::noop('{errors:descr_UNHANDLEDEXCEPTION}'),
'UNKNOWNCERT' => \SimpleSAML\Locale\Translate::noop('{errors:descr_UNKNOWNCERT}'),
'USERABORTED' => \SimpleSAML\Locale\Translate::noop('{errors:descr_USERABORTED}'),
'WRONGUSERPASS' => \SimpleSAML\Locale\Translate::noop('{errors:descr_WRONGUSERPASS}'),
);
}
/**
* Fetch all translation strings for error code descriptions.
*
* Extend this to add error codes.
*
* @return string A map from error code to error code description
*/
public static function getAllErrorCodeDescriptions()
{
return self::defaultGetAllErrorCodeDescriptions();
}
/**
* Get a map of both errorcode titles and descriptions
*
* Convenience-method for template-callers
*
* @return array An array containing both errorcode maps.
*/
public static function getAllErrorCodeMessages()
{
return array(
'title' => self::getAllErrorCodeTitles(),
'descr' => self::getAllErrorCodeDescriptions(),
);
}
/**
* Fetch a translation string for a title for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeTitle($errorCode)
{
$errorCodeTitles = self::getAllErrorCodeTitles();
return $errorCodeTitles[$errorCode];
}
/**
* Fetch a translation string for a description for a given error code.
*
* @param string $errorCode The error code to look up
*
* @return string A string to translate
*/
public static function getErrorCodeDescription($errorCode)
{
$errorCodeDescriptions = self::getAllErrorCodeDescriptions();
return $errorCodeDescriptions[$errorCode];
}
/**
* Get both title and description for a specific error code
*
* Convenience-method for template-callers
*
* @param string $errorCode The error code to look up
*
* @return array An array containing both errorcode strings.
*/
public static function getErrorCodeMessage($errorCode)
{
return array(
'title' => self::getErrorCodeTitle($errorCode),
'descr' => self::getErrorCodeDescription($errorCode),
);
}
}

View File

@@ -0,0 +1,316 @@
<?php
/**
* Base class for SimpleSAMLphp Exceptions
*
* This class tries to make sure that every exception is serializable.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_Exception extends Exception
{
/**
* The backtrace for this exception.
*
* We need to save the backtrace, since we cannot rely on
* serializing the Exception::trace-variable.
*
* @var array
*/
private $backtrace;
/**
* The cause of this exception.
*
* @var SimpleSAML_Error_Exception
*/
private $cause;
/**
* Constructor for this error.
*
* Note that the cause will be converted to a SimpleSAML_Error_UnserializableException unless it is a subclass of
* SimpleSAML_Error_Exception.
*
* @param string $message Exception message
* @param int $code Error code
* @param Exception|null $cause The cause of this exception.
*/
public function __construct($message, $code = 0, Exception $cause = null)
{
assert(is_string($message));
assert(is_int($code));
parent::__construct($message, $code);
$this->initBacktrace($this);
if ($cause !== null) {
$this->cause = SimpleSAML_Error_Exception::fromException($cause);
}
}
/**
* Convert any exception into a SimpleSAML_Error_Exception.
*
* @param Exception $e The exception.
*
* @return SimpleSAML_Error_Exception The new exception.
*/
public static function fromException(Exception $e)
{
if ($e instanceof SimpleSAML_Error_Exception) {
return $e;
}
return new SimpleSAML_Error_UnserializableException($e);
}
/**
* Load the backtrace from the given exception.
*
* @param Exception $exception The exception we should fetch the backtrace from.
*/
protected function initBacktrace(Exception $exception)
{
$this->backtrace = array();
// position in the top function on the stack
$pos = $exception->getFile().':'.$exception->getLine();
foreach ($exception->getTrace() as $t) {
$function = $t['function'];
if (array_key_exists('class', $t)) {
$function = $t['class'].'::'.$function;
}
$this->backtrace[] = $pos.' ('.$function.')';
if (array_key_exists('file', $t)) {
$pos = $t['file'].':'.$t['line'];
} else {
$pos = '[builtin]';
}
}
$this->backtrace[] = $pos.' (N/A)';
}
/**
* Retrieve the backtrace.
*
* @return array An array where each function call is a single item.
*/
public function getBacktrace()
{
return $this->backtrace;
}
/**
* Retrieve the cause of this exception.
*
* @return SimpleSAML_Error_Exception|null The cause of this exception.
*/
public function getCause()
{
return $this->cause;
}
/**
* Retrieve the class of this exception.
*
* @return string The name of the class.
*/
public function getClass()
{
return get_class($this);
}
/**
* Format this exception for logging.
*
* Create an array of lines for logging.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array Log lines that should be written out.
*/
public function format($anonymize = false)
{
$ret = array(
$this->getClass().': '.$this->getMessage(),
);
return array_merge($ret, $this->formatBacktrace($anonymize));
}
/**
* Format the backtrace for logging.
*
* Create an array of lines for logging from the backtrace.
*
* @param boolean $anonymize Whether the resulting messages should be anonymized or not.
*
* @return array All lines of the backtrace, properly formatted.
*/
public function formatBacktrace($anonymize = false)
{
$ret = array();
$basedir = SimpleSAML_Configuration::getInstance()->getBaseDir();
$e = $this;
do {
if ($e !== $this) {
$ret[] = 'Caused by: '.$e->getClass().': '.$e->getMessage();
}
$ret[] = 'Backtrace:';
$depth = count($e->backtrace);
foreach ($e->backtrace as $i => $trace) {
if ($anonymize) {
$trace = str_replace($basedir, '', $trace);
}
$ret[] = ($depth - $i - 1).' '.$trace;
}
$e = $e->cause;
} while ($e !== null);
return $ret;
}
/**
* Print the backtrace to the log if the 'debug' option is enabled in the configuration.
*/
protected function logBacktrace($level = \SimpleSAML\Logger::DEBUG)
{
// see if debugging is enabled for backtraces
$debug = SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('backtraces' => false));
if (!(in_array('backtraces', $debug, true) // implicitly enabled
|| (array_key_exists('backtraces', $debug) && $debug['backtraces'] === true) // explicitly set
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug) && $debug[0] === true) // old style 'debug' configuration option
)) {
return;
}
$backtrace = $this->formatBacktrace();
$callback = array('\SimpleSAML\Logger');
$functions = array(
\SimpleSAML\Logger::ERR => 'error',
\SimpleSAML\Logger::WARNING => 'warning',
\SimpleSAML\Logger::INFO => 'info',
\SimpleSAML\Logger::DEBUG => 'debug',
);
$callback[] = $functions[$level];
foreach ($backtrace as $line) {
call_user_func($callback, $line);
}
}
/**
* Print the exception to the log, by default with log level error.
*
* Override to allow errors extending this class to specify the log level themselves.
*
* @param int $default_level The log level to use if this method was not overridden.
*/
public function log($default_level)
{
$fn = array(
SimpleSAML\Logger::ERR => 'logError',
SimpleSAML\Logger::WARNING => 'logWarning',
SimpleSAML\Logger::INFO => 'logInfo',
SimpleSAML\Logger::DEBUG => 'logDebug',
);
call_user_func(array($this, $fn[$default_level]), $default_level);
}
/**
* Print the exception to the log with log level error.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logError()
{
SimpleSAML\Logger::error($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::ERR);
}
/**
* Print the exception to the log with log level warning.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logWarning()
{
SimpleSAML\Logger::warning($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::WARNING);
}
/**
* Print the exception to the log with log level info.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logInfo()
{
SimpleSAML\Logger::info($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::INFO);
}
/**
* Print the exception to the log with log level debug.
*
* This function will write this exception to the log, including a full backtrace.
*/
public function logDebug()
{
SimpleSAML\Logger::debug($this->getClass().': '.$this->getMessage());
$this->logBacktrace(\SimpleSAML\Logger::DEBUG);
}
/**
* Function for serialization.
*
* This function builds a list of all variables which should be serialized. It will serialize all variables except
* the Exception::trace variable.
*
* @return array Array with the variables that should be serialized.
*/
public function __sleep()
{
$ret = array_keys((array) $this);
foreach ($ret as $i => $e) {
if ($e === "\0Exception\0trace") {
unset($ret[$i]);
}
}
return $ret;
}
}

View File

@@ -0,0 +1,12 @@
<?php
/**
* Exception indicating wrong password given by user.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_InvalidCredential extends SimpleSAML_Error_User
{
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Error for missing metadata.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_MetadataNotFound extends SimpleSAML_Error_Error
{
/**
* Create the error
*
* @param string $entityId The entityID we were unable to locate.
*/
public function __construct($entityId)
{
assert(is_string($entityId));
$this->includeTemplate = 'core:no_metadata.tpl.php';
parent::__construct(array(
'METADATANOTFOUND',
'%ENTITYID%' => htmlspecialchars(var_export($entityId, true))
));
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Class SimpleSAML_Error_NoPassive
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* SimpleSAML\Module\saml\Error\NoPassive instead.
*
* @see \SimpleSAML\Module\saml\Error\NoPassive
*/
class SimpleSAML_Error_NoPassive extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Exception which will show a page telling the user
* that we don't know what to do.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_NoState extends SimpleSAML_Error_Error
{
/**
* Create the error
*/
public function __construct()
{
$this->includeTemplate = 'core:no_state.tpl.php';
parent::__construct('NOSTATE');
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Exception which will show a 404 Not Found error page.
*
* This exception can be thrown from within a module page handler. The user will then be shown a 404 Not Found error
* page.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_NotFound extends SimpleSAML_Error_Error
{
/**
* Reason why the given page could not be found.
*/
private $reason;
/**
* Create a new NotFound error
*
* @param string $reason Optional description of why the given page could not be found.
*/
public function __construct($reason = null)
{
assert($reason === null || is_string($reason));
$url = \SimpleSAML\Utils\HTTP::getSelfURL();
if ($reason === null) {
parent::__construct(array('NOTFOUND', '%URL%' => $url));
$this->message = "The requested page '$url' could not be found.";
} else {
parent::__construct(array('NOTFOUNDREASON', '%URL%' => $url, '%REASON%' => $reason));
$this->message = "The requested page '$url' could not be found. ".$reason;
}
$this->reason = $reason;
$this->httpCode = 404;
}
/**
* Retrieve the reason why the given page could not be found.
*
* @return string|null The reason why the page could not be found.
*/
public function getReason()
{
return $this->reason;
}
/**
* NotFound exceptions don't need to display a backtrace, as they are very simple and the trace is usually trivial,
* so just log the message without any backtrace at all.
*
* @param bool $anonymize Whether to anonymize the trace or not.
*
* @return array
*/
public function format($anonymize = false)
{
return array(
$this->getClass().': '.$this->getMessage(),
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* Class SimpleSAML_Error_ProxyCountExceeded
*
* @deprecated This class has been deprecated and will be removed in SimpleSAMLphp 2.0. Please use
* SimpleSAML\Module\saml\Error\ProxyCountExceeded instead.
*
* @see \SimpleSAML\Module\saml\Error\ProxyCountExceeded
*/
class SimpleSAML_Error_ProxyCountExceeded extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,57 @@
<?php
/**
* Class for saving normal exceptions for serialization.
*
* This class is used by the SimpleSAML_Auth_State class when it needs
* to serialize an exception which doesn't subclass the
* SimpleSAML_Error_Exception class.
*
* It creates a new exception which contains the backtrace and message
* of the original exception.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_UnserializableException extends SimpleSAML_Error_Exception
{
/**
* The classname of the original exception.
*
* @var string
*/
private $class;
/**
* Create a serializable exception representing an unserializable exception.
*
* @param Exception $original The original exception.
*/
public function __construct(Exception $original)
{
$this->class = get_class($original);
$msg = $original->getMessage();
$code = $original->getCode();
if (!is_int($code)) {
// PDOException uses a string as the code. Filter it out here.
$code = -1;
}
parent::__construct($msg, $code);
$this->initBacktrace($original);
}
/**
* Retrieve the class of this exception.
*
* @return string The classname.
*/
public function getClass()
{
return $this->class;
}
}

14
lib/SimpleSAML/Error/User.php Executable file
View File

@@ -0,0 +1,14 @@
<?php
/**
* Baseclass for user error exceptions
*
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_User extends SimpleSAML_Error_Exception
{
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Exception indicating user aborting the authentication process.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Error_UserAborted extends SimpleSAML_Error_Error
{
/**
* Create the error
*
* @param Exception|null $cause The exception that caused this error.
*/
public function __construct(Exception $cause = null)
{
parent::__construct('USERABORTED', $cause);
}
}

View File

@@ -0,0 +1,13 @@
<?php
/**
* Exception indicating user not found by authsource.
*
* @author Thomas Graff <thomas.graff@uninett.no>
* @package SimpleSAMLphp_base
*
*/
class SimpleSAML_Error_UserNotFound extends SimpleSAML_Error_User
{
}

558
lib/SimpleSAML/IdP.php Executable file
View File

@@ -0,0 +1,558 @@
<?php
/**
* IdP class.
*
* This class implements the various functions used by IdP.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_IdP
{
/**
* A cache for resolving IdP id's.
*
* @var array
*/
private static $idpCache = array();
/**
* The identifier for this IdP.
*
* @var string
*/
private $id;
/**
* The "association group" for this IdP.
*
* We use this to support cross-protocol logout until
* we implement a cross-protocol IdP.
*
* @var string
*/
private $associationGroup;
/**
* The configuration for this IdP.
*
* @var SimpleSAML_Configuration
*/
private $config;
/**
* Our authsource.
*
* @var \SimpleSAML\Auth\Simple
*/
private $authSource;
/**
* Initialize an IdP.
*
* @param string $id The identifier of this IdP.
*
* @throws SimpleSAML_Error_Exception If the IdP is disabled or no such auth source was found.
*/
private function __construct($id)
{
assert(is_string($id));
$this->id = $id;
$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$globalConfig = SimpleSAML_Configuration::getInstance();
if (substr($id, 0, 6) === 'saml2:') {
if (!$globalConfig->getBoolean('enable.saml20-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.saml20-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'saml20-idp-hosted');
} elseif (substr($id, 0, 6) === 'saml1:') {
if (!$globalConfig->getBoolean('enable.shib13-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.shib13-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 6), 'shib13-idp-hosted');
} elseif (substr($id, 0, 5) === 'adfs:') {
if (!$globalConfig->getBoolean('enable.adfs-idp', false)) {
throw new SimpleSAML_Error_Exception('enable.adfs-idp disabled in config.php.');
}
$this->config = $metadata->getMetaDataConfig(substr($id, 5), 'adfs-idp-hosted');
try {
// this makes the ADFS IdP use the same SP associations as the SAML 2.0 IdP
$saml2EntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
$this->associationGroup = 'saml2:'.$saml2EntityId;
} catch (Exception $e) {
// probably no SAML 2 IdP configured for this host. Ignore the error
}
} else {
assert(false);
}
if ($this->associationGroup === null) {
$this->associationGroup = $this->id;
}
$auth = $this->config->getString('auth');
if (SimpleSAML_Auth_Source::getById($auth) !== null) {
$this->authSource = new \SimpleSAML\Auth\Simple($auth);
} else {
throw new SimpleSAML_Error_Exception('No such "'.$auth.'" auth source found.');
}
}
/**
* Retrieve the ID of this IdP.
*
* @return string The ID of this IdP.
*/
public function getId()
{
return $this->id;
}
/**
* Retrieve an IdP by ID.
*
* @param string $id The identifier of the IdP.
*
* @return SimpleSAML_IdP The IdP.
*/
public static function getById($id)
{
assert(is_string($id));
if (isset(self::$idpCache[$id])) {
return self::$idpCache[$id];
}
$idp = new self($id);
self::$idpCache[$id] = $idp;
return $idp;
}
/**
* Retrieve the IdP "owning" the state.
*
* @param array &$state The state array.
*
* @return SimpleSAML_IdP The IdP.
*/
public static function getByState(array &$state)
{
assert(isset($state['core:IdP']));
return self::getById($state['core:IdP']);
}
/**
* Retrieve the configuration for this IdP.
*
* @return SimpleSAML_Configuration The configuration object.
*/
public function getConfig()
{
return $this->config;
}
/**
* Get SP name.
*
* @param string $assocId The association identifier.
*
* @return array|null The name of the SP, as an associative array of language => text, or null if this isn't an SP.
*/
public function getSPName($assocId)
{
assert(is_string($assocId));
$prefix = substr($assocId, 0, 4);
$spEntityId = substr($assocId, strlen($prefix) + 1);
$metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
if ($prefix === 'saml') {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
} catch (Exception $e) {
try {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'shib13-sp-remote');
} catch (Exception $e) {
return null;
}
}
} else {
if ($prefix === 'adfs') {
$spMetadata = $metadata->getMetaDataConfig($spEntityId, 'adfs-sp-remote');
} else {
return null;
}
}
if ($spMetadata->hasValue('name')) {
return $spMetadata->getLocalizedString('name');
} elseif ($spMetadata->hasValue('OrganizationDisplayName')) {
return $spMetadata->getLocalizedString('OrganizationDisplayName');
} else {
return array('en' => $spEntityId);
}
}
/**
* Add an SP association.
*
* @param array $association The SP association.
*/
public function addAssociation(array $association)
{
assert(isset($association['id']));
assert(isset($association['Handler']));
$association['core:IdP'] = $this->id;
$session = SimpleSAML_Session::getSessionFromRequest();
$session->addAssociation($this->associationGroup, $association);
}
/**
* Retrieve list of SP associations.
*
* @return array List of SP associations.
*/
public function getAssociations()
{
$session = SimpleSAML_Session::getSessionFromRequest();
return $session->getAssociations($this->associationGroup);
}
/**
* Remove an SP association.
*
* @param string $assocId The association id.
*/
public function terminateAssociation($assocId)
{
assert(is_string($assocId));
$session = SimpleSAML_Session::getSessionFromRequest();
$session->terminateAssociation($this->associationGroup, $assocId);
}
/**
* Is the current user authenticated?
*
* @return boolean True if the user is authenticated, false otherwise.
*/
public function isAuthenticated()
{
return $this->authSource->isAuthenticated();
}
/**
* Called after authproc has run.
*
* @param array $state The authentication request state array.
*/
public static function postAuthProc(array $state)
{
assert(is_callable($state['Responder']));
if (isset($state['core:SP'])) {
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData(
'core:idp-ssotime',
$state['core:IdP'].';'.$state['core:SP'],
time(),
SimpleSAML_Session::DATA_TIMEOUT_SESSION_END
);
}
call_user_func($state['Responder'], $state);
assert(false);
}
/**
* The user is authenticated.
*
* @param array $state The authentication request state array.
*
* @throws SimpleSAML_Error_Exception If we are not authenticated.
*/
public static function postAuth(array $state)
{
$idp = SimpleSAML_IdP::getByState($state);
if (!$idp->isAuthenticated()) {
throw new SimpleSAML_Error_Exception('Not authenticated.');
}
$state['Attributes'] = $idp->authSource->getAttributes();
if (isset($state['SPMetadata'])) {
$spMetadata = $state['SPMetadata'];
} else {
$spMetadata = array();
}
if (isset($state['core:SP'])) {
$session = SimpleSAML_Session::getSessionFromRequest();
$previousSSOTime = $session->getData('core:idp-ssotime', $state['core:IdP'].';'.$state['core:SP']);
if ($previousSSOTime !== null) {
$state['PreviousSSOTimestamp'] = $previousSSOTime;
}
}
$idpMetadata = $idp->getConfig()->toArray();
$pc = new SimpleSAML_Auth_ProcessingChain($idpMetadata, $spMetadata, 'idp');
$state['ReturnCall'] = array('SimpleSAML_IdP', 'postAuthProc');
$state['Destination'] = $spMetadata;
$state['Source'] = $idpMetadata;
$pc->processState($state);
self::postAuthProc($state);
}
/**
* Authenticate the user.
*
* This function authenticates the user.
*
* @param array &$state The authentication request state.
*
* @throws \SimpleSAML\Module\saml\Error\NoPassive If we were asked to do passive authentication.
*/
private function authenticate(array &$state)
{
if (isset($state['isPassive']) && (bool) $state['isPassive']) {
throw new \SimpleSAML\Module\saml\Error\NoPassive('Passive authentication not supported.');
}
$this->authSource->login($state);
}
/**
* Re-authenticate the user.
*
* This function re-authenticates an user with an existing session. This gives the authentication source a chance
* to do additional work when re-authenticating for SSO.
*
* Note: This function is not used when ForceAuthn=true.
*
* @param array &$state The authentication request state.
*
* @throws SimpleSAML_Error_Exception If there is no auth source defined for this IdP.
*/
private function reauthenticate(array &$state)
{
$sourceImpl = $this->authSource->getAuthSource();
if ($sourceImpl === null) {
throw new SimpleSAML_Error_Exception('No such auth source defined.');
}
$sourceImpl->reauthenticate($state);
}
/**
* Process authentication requests.
*
* @param array &$state The authentication request state.
*/
public function handleAuthenticationRequest(array &$state)
{
assert(isset($state['Responder']));
$state['core:IdP'] = $this->id;
if (isset($state['SPMetadata']['entityid'])) {
$spEntityId = $state['SPMetadata']['entityid'];
} elseif (isset($state['SPMetadata']['entityID'])) {
$spEntityId = $state['SPMetadata']['entityID'];
} else {
$spEntityId = null;
}
$state['core:SP'] = $spEntityId;
// first, check whether we need to authenticate the user
if (isset($state['ForceAuthn']) && (bool) $state['ForceAuthn']) {
// force authentication is in effect
$needAuth = true;
} else {
$needAuth = !$this->isAuthenticated();
}
$state['IdPMetadata'] = $this->getConfig()->toArray();
$state['ReturnCallback'] = array('SimpleSAML_IdP', 'postAuth');
try {
if ($needAuth) {
$this->authenticate($state);
assert(false);
} else {
$this->reauthenticate($state);
}
$this->postAuth($state);
} catch (SimpleSAML_Error_Exception $e) {
SimpleSAML_Auth_State::throwException($state, $e);
} catch (Exception $e) {
$e = new SimpleSAML_Error_UnserializableException($e);
SimpleSAML_Auth_State::throwException($state, $e);
}
}
/**
* Find the logout handler of this IdP.
*
* @return \SimpleSAML\IdP\LogoutHandlerInterface The logout handler class.
*
* @throws SimpleSAML_Error_Exception If we cannot find a logout handler.
*/
public function getLogoutHandler()
{
// find the logout handler
$logouttype = $this->getConfig()->getString('logouttype', 'traditional');
switch ($logouttype) {
case 'traditional':
$handler = 'SimpleSAML\IdP\TraditionalLogoutHandler';
break;
case 'iframe':
$handler = 'SimpleSAML\IdP\IFrameLogoutHandler';
break;
default:
throw new SimpleSAML_Error_Exception('Unknown logout handler: '.var_export($logouttype, true));
}
return new $handler($this);
}
/**
* Finish the logout operation.
*
* This function will never return.
*
* @param array &$state The logout request state.
*/
public function finishLogout(array &$state)
{
assert(isset($state['Responder']));
$idp = SimpleSAML_IdP::getByState($state);
call_user_func($state['Responder'], $idp, $state);
assert(false);
}
/**
* Process a logout request.
*
* This function will never return.
*
* @param array &$state The logout request state.
* @param string|null $assocId The association we received the logout request from, or null if there was no
* association.
*/
public function handleLogoutRequest(array &$state, $assocId)
{
assert(isset($state['Responder']));
assert(is_string($assocId) || $assocId === null);
$state['core:IdP'] = $this->id;
$state['core:TerminatedAssocId'] = $assocId;
if ($assocId !== null) {
$this->terminateAssociation($assocId);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id.':'.$state['saml:SPEntityId']);
}
// terminate the local session
$id = SimpleSAML_Auth_State::saveState($state, 'core:Logout:afterbridge');
$returnTo = SimpleSAML\Module::getModuleURL('core/idp/resumelogout.php', array('id' => $id));
$this->authSource->logout($returnTo);
$handler = $this->getLogoutHandler();
$handler->startLogout($state, $assocId);
assert(false);
}
/**
* Process a logout response.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function handleLogoutResponse($assocId, $relayState, SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->deleteData('core:idp-ssotime', $this->id.';'.substr($assocId, strpos($assocId, ':') + 1));
$handler = $this->getLogoutHandler();
$handler->onResponse($assocId, $relayState, $error);
assert(false);
}
/**
* Log out, then redirect to a URL.
*
* This function never returns.
*
* @param string $url The URL the user should be returned to after logout.
*/
public function doLogoutRedirect($url)
{
assert(is_string($url));
$state = array(
'Responder' => array('SimpleSAML_IdP', 'finishLogoutRedirect'),
'core:Logout:URL' => $url,
);
$this->handleLogoutRequest($state, null);
assert(false);
}
/**
* Redirect to a URL after logout.
*
* This function never returns.
*
* @param SimpleSAML_IdP $idp Deprecated. Will be removed.
* @param array &$state The logout state from doLogoutRedirect().
*/
public static function finishLogoutRedirect(SimpleSAML_IdP $idp, array $state)
{
assert(isset($state['core:Logout:URL']));
\SimpleSAML\Utils\HTTP::redirectTrustedURL($state['core:Logout:URL']);
assert(false);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\IdP;
use SimpleSAML\Module;
use SimpleSAML\Utils\HTTP;
/**
* Class that handles iframe logout.
*
* @package SimpleSAMLphp
*/
class IFrameLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var \SimpleSAML_IdP
*/
private $idp;
/**
* LogoutIFrame constructor.
*
* @param \SimpleSAML_IdP $idp The IdP to log out from.
*/
public function __construct(\SimpleSAML_IdP $idp)
{
$this->idp = $idp;
}
/**
* Start the logout operation.
*
* @param array &$state The logout state.
* @param string|null $assocId The SP we are logging out from.
*/
public function startLogout(array &$state, $assocId)
{
assert(is_string($assocId) || $assocId === null);
$associations = $this->idp->getAssociations();
if (count($associations) === 0) {
$this->idp->finishLogout($state);
}
foreach ($associations as $id => &$association) {
$idp = \SimpleSAML_IdP::getByState($association);
$association['core:Logout-IFrame:Name'] = $idp->getSPName($id);
$association['core:Logout-IFrame:State'] = 'onhold';
}
$state['core:Logout-IFrame:Associations'] = $associations;
if (!is_null($assocId)) {
$spName = $this->idp->getSPName($assocId);
if ($spName === null) {
$spName = array('en' => $assocId);
}
$state['core:Logout-IFrame:From'] = $spName;
} else {
$state['core:Logout-IFrame:From'] = null;
}
$params = array(
'id' => \SimpleSAML_Auth_State::saveState($state, 'core:Logout-IFrame'),
);
if (isset($state['core:Logout-IFrame:InitType'])) {
$params['type'] = $state['core:Logout-IFrame:InitType'];
}
$url = Module::getModuleURL('core/idp/logout-iframe.php', $params);
HTTP::redirectTrustedURL($url);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
$spId = sha1($assocId);
$this->idp->terminateAssociation($assocId);
$header = <<<HEADER
<!DOCTYPE html>
<html>
<head>
<title>Logout response from %s</title>
<script>
HEADER;
printf($header, htmlspecialchars(var_export($assocId, true)));
if ($error) {
$errorMsg = $error->getMessage();
echo('window.parent.logoutFailed("'.$spId.'", "'.addslashes($errorMsg).'");');
} else {
echo('window.parent.logoutCompleted("'.$spId.'");');
}
echo <<<FOOTER
</script>
</head>
<body>
</body>
</html>
FOOTER;
exit(0);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace SimpleSAML\IdP;
/**
* Interface that all logout handlers must implement.
*
* @package SimpleSAMLphp
*/
interface LogoutHandlerInterface
{
/**
* Initialize this logout handler.
*
* @param \SimpleSAML_IdP $idp The IdP we are logging out from.
*/
public function __construct(\SimpleSAML_IdP $idp);
/**
* Start a logout operation.
*
* This function must never return.
*
* @param array &$state The logout state.
* @param string|null $assocId The association that started the logout.
*/
public function startLogout(array &$state, $assocId);
/**
* Handles responses to our logout requests.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null);
}

View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\IdP;
use SimpleSAML\Logger;
use SimpleSAML\Utils\HTTP;
/**
* Class that handles traditional logout.
*
* @package SimpleSAMLphp
*/
class TraditionalLogoutHandler implements LogoutHandlerInterface
{
/**
* The IdP we are logging out from.
*
* @var \SimpleSAML_IdP
*/
private $idp;
/**
* TraditionalLogout constructor.
*
* @param \SimpleSAML_IdP $idp The IdP to log out from.
*/
public function __construct(\SimpleSAML_IdP $idp)
{
$this->idp = $idp;
}
/**
* Picks the next SP and issues a logout request.
*
* This function never returns.
*
* @param array &$state The logout state.
*/
private function logoutNextSP(array &$state)
{
$association = array_pop($state['core:LogoutTraditional:Remaining']);
if ($association === null) {
$this->idp->finishLogout($state);
}
$relayState = \SimpleSAML_Auth_State::saveState($state, 'core:LogoutTraditional', true);
$id = $association['id'];
Logger::info('Logging out of '.var_export($id, true).'.');
try {
$idp = \SimpleSAML_IdP::getByState($association);
$url = call_user_func(array($association['Handler'], 'getLogoutURL'), $idp, $association, $relayState);
HTTP::redirectTrustedURL($url);
} catch (\Exception $e) {
Logger::warning('Unable to initialize logout to '.var_export($id, true).'.');
$this->idp->terminateAssociation($id);
$state['core:Failed'] = true;
// Try the next SP
$this->logoutNextSP($state);
assert(false);
}
}
/**
* Start the logout operation.
*
* This function never returns.
*
* @param array &$state The logout state.
* @param string $assocId The association that started the logout.
*/
public function startLogout(array &$state, $assocId)
{
$state['core:LogoutTraditional:Remaining'] = $this->idp->getAssociations();
$this->logoutNextSP($state);
}
/**
* Continue the logout operation.
*
* This function will never return.
*
* @param string $assocId The association that is terminated.
* @param string|null $relayState The RelayState from the start of the logout.
* @param \SimpleSAML_Error_Exception|null $error The error that occurred during session termination (if any).
*
* @throws \SimpleSAML_Error_Exception If the RelayState was lost during logout.
*/
public function onResponse($assocId, $relayState, \SimpleSAML_Error_Exception $error = null)
{
assert(is_string($assocId));
assert(is_string($relayState) || $relayState === null);
if ($relayState === null) {
throw new \SimpleSAML_Error_Exception('RelayState lost during logout.');
}
$state = \SimpleSAML_Auth_State::loadState($relayState, 'core:LogoutTraditional');
if ($error === null) {
Logger::info('Logged out of '.var_export($assocId, true).'.');
$this->idp->terminateAssociation($assocId);
} else {
Logger::warning('Error received from '.var_export($assocId, true).' during logout:');
$error->logWarning();
$state['core:Failed'] = true;
}
$this->logoutNextSP($state);
}
}

View File

@@ -0,0 +1,424 @@
<?php
/**
* Choosing the language to localize to for our minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
use SimpleSAML\Utils\HTTP;
class Language
{
/**
* This is the default language map. It is used to map languages codes from the user agent to other language codes.
*/
private static $defaultLanguageMap = array('nb' => 'no');
/**
* The configuration to use.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
/**
* An array holding a list of languages available.
*
* @var array
*/
private $availableLanguages;
/**
* The language currently in use.
*
* @var null|string
*/
private $language = null;
/**
* The language to use by default.
*
* @var string
*/
private $defaultLanguage;
/**
* An array holding a list of languages that are written from right to left.
*
* @var array
*/
private $rtlLanguages;
/**
* HTTP GET language parameter name.
*
* @var string
*/
private $languageParameterName;
/**
* A custom function to use in order to determine the language in use.
*
* @var callable|null
*/
private $customFunction;
/**
* A list of languages supported with their names localized.
* Indexed by something that mostly resembles ISO 639-1 code,
* with some charming SimpleSAML-specific variants...
* that must remain before 2.0 due to backwards compatibility
*
* @var array
*/
private $language_names = array(
'no' => 'Bokmål', // Norwegian Bokmål
'nn' => 'Nynorsk', // Norwegian Nynorsk
'se' => 'Sámegiella', // Northern Sami
'sma' => 'Åarjelh-saemien giele', // Southern Sami
'da' => 'Dansk', // Danish
'en' => 'English',
'de' => 'Deutsch', // German
'sv' => 'Svenska', // Swedish
'fi' => 'Suomeksi', // Finnish
'es' => 'Español', // Spanish
'ca' => 'Català', // Catalan
'fr' => 'Français', // French
'it' => 'Italiano', // Italian
'nl' => 'Nederlands', // Dutch
'lb' => 'Lëtzebuergesch', // Luxembourgish
'cs' => 'Čeština', // Czech
'sl' => 'Slovenščina', // Slovensk
'lt' => 'Lietuvių kalba', // Lithuanian
'hr' => 'Hrvatski', // Croatian
'hu' => 'Magyar', // Hungarian
'pl' => 'Język polski', // Polish
'pt' => 'Português', // Portuguese
'pt-br' => 'Português brasileiro', // Portuguese
'ru' => 'русский язык', // Russian
'et' => 'eesti keel', // Estonian
'tr' => 'Türkçe', // Turkish
'el' => 'ελληνικά', // Greek
'ja' => '日本語', // Japanese
'zh' => '简体中文', // Chinese (simplified)
'zh-tw' => '繁體中文', // Chinese (traditional)
'ar' => 'العربية', // Arabic
'fa' => 'پارسی', // Persian
'ur' => 'اردو', // Urdu
'he' => 'עִבְרִית', // Hebrew
'id' => 'Bahasa Indonesia', // Indonesian
'sr' => 'Srpski', // Serbian
'lv' => 'Latviešu', // Latvian
'ro' => 'Românește', // Romanian
'eu' => 'Euskara', // Basque
'af' => 'Afrikaans', // Afrikaans
);
/**
* A mapping of SSP languages to locales
*
* @var array
*/
private $languagePosixMapping = array(
'no' => 'nb_NO',
'nn' => 'nn_NO',
);
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
*/
public function __construct(\SimpleSAML_Configuration $configuration)
{
$this->configuration = $configuration;
$this->availableLanguages = $this->getInstalledLanguages();
$this->defaultLanguage = $this->configuration->getString('language.default', 'en');
$this->languageParameterName = $this->configuration->getString('language.parameter.name', 'language');
$this->customFunction = $this->configuration->getArray('language.get_language_function', null);
$this->rtlLanguages = $this->configuration->getArray('language.rtl', array());
if (isset($_GET[$this->languageParameterName])) {
$this->setLanguage(
$_GET[$this->languageParameterName],
$this->configuration->getBoolean('language.parameter.setcookie', true)
);
}
}
/**
* Filter configured (available) languages against installed languages.
*
* @return array The set of languages both in 'language.available' and $this->language_names.
*/
private function getInstalledLanguages()
{
$configuredAvailableLanguages = $this->configuration->getArray('language.available', array('en'));
$availableLanguages = array();
foreach ($configuredAvailableLanguages as $code) {
if (array_key_exists($code, $this->language_names) && isset($this->language_names[$code])) {
$availableLanguages[] = $code;
} else {
\SimpleSAML\Logger::error("Language \"$code\" not installed. Check config.");
}
}
return $availableLanguages;
}
/**
* Rename to non-idiosyncratic language code.
*
* @param string $language Language code for the language to rename, if necessary.
*
* @return string The language code.
*/
public function getPosixLanguage($language)
{
if (isset($this->languagePosixMapping[$language])) {
return $this->languagePosixMapping[$language];
}
return $language;
}
/**
* This method will set a cookie for the user's browser to remember what language was selected.
*
* @param string $language Language code for the language to set.
* @param boolean $setLanguageCookie Whether to set the language cookie or not. Defaults to true.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$language = strtolower($language);
if (in_array($language, $this->availableLanguages, true)) {
$this->language = $language;
if ($setLanguageCookie === true) {
self::setLanguageCookie($language);
}
}
}
/**
* This method will return the language selected by the user, or the default language. It looks first for a cached
* language code, then checks for a language cookie, then it tries to calculate the preferred language from HTTP
* headers.
*
* @return string The language selected by the user according to the processing rules specified, or the default
* language in any other case.
*/
public function getLanguage()
{
// language is set in object
if (isset($this->language)) {
return $this->language;
}
// run custom getLanguage function if defined
if (isset($this->customFunction) && is_callable($this->customFunction)) {
$customLanguage = call_user_func($this->customFunction, $this);
if ($customLanguage !== null && $customLanguage !== false) {
return $customLanguage;
}
}
// language is provided in a stored cookie
$languageCookie = Language::getLanguageCookie();
if ($languageCookie !== null) {
$this->language = $languageCookie;
return $languageCookie;
}
// check if we can find a good language from the Accept-Language HTTP header
$httpLanguage = $this->getHTTPLanguage();
if ($httpLanguage !== null) {
return $httpLanguage;
}
// language is not set, and we get the default language from the configuration
return $this->getDefaultLanguage();
}
/**
* Get the localized name of a language, by ISO 639-2 code.
*
* @param string $code The ISO 639-2 code of the language.
*
* @return string The localized name of the language.
*/
public function getLanguageLocalizedName($code)
{
if (array_key_exists($code, $this->language_names) && isset($this->language_names[$code])) {
return $this->language_names[$code];
}
\SimpleSAML\Logger::error("Name for language \"$code\" not found. Check config.");
return null;
}
/**
* Get the language parameter name.
*
* @return string The language parameter name.
*/
public function getLanguageParameterName()
{
return $this->languageParameterName;
}
/**
* This method returns the preferred language for the user based on the Accept-Language HTTP header.
*
* @return string The preferred language based on the Accept-Language HTTP header, or null if none of the languages
* in the header is available.
*/
private function getHTTPLanguage()
{
$languageScore = HTTP::getAcceptLanguage();
// for now we only use the default language map. We may use a configurable language map in the future
$languageMap = self::$defaultLanguageMap;
// find the available language with the best score
$bestLanguage = null;
$bestScore = -1.0;
foreach ($languageScore as $language => $score) {
// apply the language map to the language code
if (array_key_exists($language, $languageMap)) {
$language = $languageMap[$language];
}
if (!in_array($language, $this->availableLanguages, true)) {
// skip this language - we don't have it
continue;
}
/* Some user agents use very limited precision of the quality value, but order the elements in descending
* order. Therefore we rely on the order of the output from getAcceptLanguage() matching the order of the
* languages in the header when two languages have the same quality.
*/
if ($score > $bestScore) {
$bestLanguage = $language;
$bestScore = $score;
}
}
return $bestLanguage;
}
/**
* Return the default language according to configuration.
*
* @return string The default language that has been configured. Defaults to english if not configured.
*/
public function getDefaultLanguage()
{
return $this->defaultLanguage;
}
/**
* Return an alias for a language code, if any.
*
* @return string The alias, or null if the alias was not found.
*/
public function getLanguageCodeAlias($langcode)
{
if (isset(self::$defaultLanguageMap[$langcode])) {
return self::$defaultLanguageMap[$langcode];
}
// No alias found, which is fine
return null;
}
/**
* Return an indexed list of all languages available.
*
* @return array An array holding all the languages available as the keys of the array. The value for each key is
* true in case that the language specified by that key is currently active, or false otherwise.
*/
public function getLanguageList()
{
$current = $this->getLanguage();
$list = array_fill_keys($this->availableLanguages, false);
$list[$current] = true;
return $list;
}
/**
* Check whether a language is written from the right to the left or not.
*
* @return boolean True if the language is right-to-left, false otherwise.
*/
public function isLanguageRTL()
{
return in_array($this->getLanguage(), $this->rtlLanguages, true);
}
/**
* Retrieve the user-selected language from a cookie.
*
* @return string|null The selected language or null if unset.
*/
public static function getLanguageCookie()
{
$config = \SimpleSAML_Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', array('en'));
$name = $config->getString('language.cookie.name', 'language');
if (isset($_COOKIE[$name])) {
$language = strtolower((string) $_COOKIE[$name]);
if (in_array($language, $availableLanguages, true)) {
return $language;
}
}
return null;
}
/**
* This method will attempt to set the user-selected language in a cookie. It will do nothing if the language
* specified is not in the list of available languages, or the headers have already been sent to the browser.
*
* @param string $language The language set by the user.
*/
public static function setLanguageCookie($language)
{
assert(is_string($language));
$language = strtolower($language);
$config = \SimpleSAML_Configuration::getInstance();
$availableLanguages = $config->getArray('language.available', array('en'));
if (!in_array($language, $availableLanguages, true) || headers_sent()) {
return;
}
$name = $config->getString('language.cookie.name', 'language');
$params = array(
'lifetime' => ($config->getInteger('language.cookie.lifetime', 60 * 60 * 24 * 900)),
'domain' => ($config->getString('language.cookie.domain', null)),
'path' => ($config->getString('language.cookie.path', '/')),
'secure' => ($config->getBoolean('language.cookie.secure', false)),
'httponly' => ($config->getBoolean('language.cookie.httponly', false)),
);
HTTP::setCookie($name, $language, $params, false);
}
}

View File

@@ -0,0 +1,265 @@
<?php
/**
* Glue to connect one or more translation/locale systems to the rest
*
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
use Gettext\Translations;
use Gettext\Translator;
class Localization
{
/**
* The configuration to use.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
/**
* The default gettext domain.
*/
const DEFAULT_DOMAIN = 'messages';
/**
* Old internationalization backend included in SimpleSAMLphp.
*/
const SSP_I18N_BACKEND = 'SimpleSAMLphp';
/**
* An internationalization backend implemented purely in PHP.
*/
const GETTEXT_I18N_BACKEND = 'gettext/gettext';
/**
* The default locale directory
*/
private $localeDir;
/**
* Where specific domains are stored
*/
private $localeDomainMap = array();
/**
* Pointer to currently active translator
*/
private $translator;
/**
* Pointer to current Language
*/
private $language;
/**
* Language code representing the current Language
*/
private $langcode;
/**
* The language backend to use
*/
public $i18nBackend;
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
*/
public function __construct(\SimpleSAML_Configuration $configuration)
{
$this->configuration = $configuration;
$this->localeDir = $this->configuration->resolvePath('locales');
$this->language = new Language($configuration);
$this->langcode = $this->language->getPosixLanguage($this->language->getLanguage());
$this->i18nBackend = $this->configuration->getString('language.i18n.backend', self::SSP_I18N_BACKEND);
$this->setupL10N();
}
/**
* Dump the default locale directory
*/
public function getLocaleDir()
{
return $this->localeDir;
}
/**
* Get the default locale dir for a specific module aka. domain
*
* @param string $domain Name of module/domain
*/
public function getDomainLocaleDir($domain)
{
$localeDir = $this->configuration->resolvePath('modules') . '/' . $domain . '/locales';
return $localeDir;
}
/*
* Add a new translation domain from a module
* (We're assuming that each domain only exists in one place)
*
* @param string $module Module name
* @param string $localeDir Absolute path if the module is housed elsewhere
*/
public function addModuleDomain($module, $localeDir = null)
{
if (!$localeDir) {
$localeDir = $this->getDomainLocaleDir($module);
}
$this->addDomain($localeDir, $module);
}
/*
* Add a new translation domain
* (We're assuming that each domain only exists in one place)
*
* @param string $localeDir Location of translations
* @param string $domain Domain at location
*/
public function addDomain($localeDir, $domain)
{
$this->localeDomainMap[$domain] = $localeDir;
\SimpleSAML\Logger::debug("Localization: load domain '$domain' at '$localeDir'");
$this->loadGettextGettextFromPO($domain);
}
/*
* Get and check path of localization file
*
* @param string $domain Name of localization domain
* @throws Exception If the path does not exist even for the default, fallback language
*/
public function getLangPath($domain = self::DEFAULT_DOMAIN)
{
$langcode = explode('_', $this->langcode);
$langcode = $langcode[0];
$localeDir = $this->localeDomainMap[$domain];
$langPath = $localeDir.'/'.$langcode.'/LC_MESSAGES/';
\SimpleSAML\Logger::debug("Trying langpath for '$langcode' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
// Some langcodes have aliases..
$alias = $this->language->getLanguageCodeAlias($langcode);
if (isset($alias)) {
$langPath = $localeDir.'/'.$alias.'/LC_MESSAGES/';
\SimpleSAML\Logger::debug("Trying langpath for alternative '$alias' as '$langPath'");
if (is_dir($langPath) && is_readable($langPath)) {
return $langPath;
}
}
// Language not found, fall back to default
$defLangcode = $this->language->getDefaultLanguage();
$langPath = $localeDir.'/'.$defLangcode.'/LC_MESSAGES/';
if (is_dir($langPath) && is_readable($langPath)) {
// Report that the localization for the preferred language is missing
$error = "Localization not found for langcode '$langcode' at '$langPath', falling back to langcode '".
$defLangcode."'";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
return $langPath;
}
// Locale for default language missing even, error out
$error = "Localization directory missing/broken for langcode '$langcode' and domain '$domain'";
\SimpleSAML\Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
throw new \Exception($error);
}
/**
* Setup the translator
*/
private function setupTranslator()
{
$this->translator = new Translator();
$this->translator->register();
}
/**
* Load translation domain from Gettext/Gettext using .po
*
* Note: Since Twig I18N does not support domains, all loaded files are
* merged. Use contexts if identical strings need to be disambiguated.
*
* @param string $domain Name of domain
* @param boolean $catchException Whether to catch an exception on error or return early
*
* @throws \Exception If something is wrong with the locale file for the domain and activated language
*/
private function loadGettextGettextFromPO($domain = self::DEFAULT_DOMAIN, $catchException = true)
{
try {
$langPath = $this->getLangPath($domain);
} catch (\Exception $e) {
$error = "Something went wrong when trying to get path to language file, cannot load domain '$domain'.";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
if ($catchException) {
// bail out!
return;
} else {
throw $e;
}
}
$poFile = $domain.'.po';
$poPath = $langPath.$poFile;
if (file_exists($poPath) && is_readable($poPath)) {
$translations = Translations::fromPoFile($poPath);
$this->translator->loadTranslations($translations);
} else {
$error = "Localization file '$poFile' not found in '$langPath', falling back to default";
\SimpleSAML\Logger::error($_SERVER['PHP_SELF'].' - '.$error);
}
}
/**
* Test to check if backend is set to default
*
* (if false: backend unset/there's an error)
*/
public function isI18NBackendDefault()
{
if ($this->i18nBackend === $this::SSP_I18N_BACKEND) {
return true;
}
return false;
}
/**
* Set up L18N if configured or fallback to old system
*/
private function setupL10N()
{
if ($this->i18nBackend === self::SSP_I18N_BACKEND) {
\SimpleSAML\Logger::debug("Localization: using old system");
return;
}
$this->setupTranslator();
// setup default domain
$this->addDomain($this->localeDir, self::DEFAULT_DOMAIN);
}
/**
* Show which domains are registered
*/
public function getRegisteredDomains()
{
return $this->localeDomainMap;
}
}

View File

@@ -0,0 +1,546 @@
<?php
/**
* The translation-relevant bits from our original minimalistic XHTML PHP based template system.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Hanne Moa, UNINETT AS. <hanne.moa@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Locale;
class Translate
{
/**
* The configuration to be used for this translator.
*
* @var \SimpleSAML_Configuration
*/
private $configuration;
private $langtext = array();
/**
* Associative array of dictionaries.
*/
private $dictionaries = array();
/**
* The default dictionary.
*/
private $defaultDictionary = null;
/**
* The language object we'll use internally.
*
* @var \SimpleSAML\Locale\Language
*/
private $language;
/**
* Constructor
*
* @param \SimpleSAML_Configuration $configuration Configuration object
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(\SimpleSAML_Configuration $configuration, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->language = new Language($configuration);
if ($defaultDictionary !== null && substr($defaultDictionary, -4) === '.php') {
// TODO: drop this entire if clause for 2.0
// for backwards compatibility - print warning
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate(...) at '.$where.
'. The last parameter is now a dictionary name, which should not end in ".php".'
);
$this->defaultDictionary = substr($defaultDictionary, 0, -4);
} else {
$this->defaultDictionary = $defaultDictionary;
}
}
/**
* Return the internal language object used by this translator.
*
* @return \SimpleSAML\Locale\Language
*/
public function getLanguage()
{
return $this->language;
}
/**
* This method retrieves a dictionary with the name given.
*
* @param string $name The name of the dictionary, as the filename in the dictionary directory, without the
* '.php' ending.
*
* @return array An associative array with the dictionary.
*/
private function getDictionary($name)
{
assert(is_string($name));
if (!array_key_exists($name, $this->dictionaries)) {
$sepPos = strpos($name, ':');
if ($sepPos !== false) {
$module = substr($name, 0, $sepPos);
$fileName = substr($name, $sepPos + 1);
$dictDir = \SimpleSAML\Module::getModuleDir($module).'/dictionaries/';
} else {
$dictDir = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
$fileName = $name;
}
$this->dictionaries[$name] = $this->readDictionaryFile($dictDir.$fileName);
}
return $this->dictionaries[$name];
}
/**
* This method retrieves a tag as an array with language => string mappings.
*
* @param string $tag The tag name. The tag name can also be on the form '{<dictionary>:<tag>}', to retrieve a tag
* from the specific dictionary.
*
* @return array An associative array with language => string mappings, or null if the tag wasn't found.
*/
public function getTag($tag)
{
assert(is_string($tag));
// first check translations loaded by the includeInlineTranslation and includeLanguageFile methods
if (array_key_exists($tag, $this->langtext)) {
return $this->langtext[$tag];
}
// check whether we should use the default dictionary or a dictionary specified in the tag
if (substr($tag, 0, 1) === '{' && preg_match('/^{((?:\w+:)?\w+?):(.*)}$/D', $tag, $matches)) {
$dictionary = $matches[1];
$tag = $matches[2];
} else {
$dictionary = $this->defaultDictionary;
if ($dictionary === null) {
// we don't have any dictionary to load the tag from
return null;
}
}
$dictionary = $this->getDictionary($dictionary);
if (!array_key_exists($tag, $dictionary)) {
return null;
}
return $dictionary[$tag];
}
/**
* Retrieve the preferred translation of a given text.
*
* @param array $translations The translations, as an associative array with language => text mappings.
*
* @return string The preferred translation.
*
* @throws \Exception If there's no suitable translation.
*/
public function getPreferredTranslation($translations)
{
assert(is_array($translations));
// look up translation of tag in the selected language
$selected_language = $this->language->getLanguage();
if (array_key_exists($selected_language, $translations)) {
return $translations[$selected_language];
}
// look up translation of tag in the default language
$default_language = $this->language->getDefaultLanguage();
if (array_key_exists($default_language, $translations)) {
return $translations[$default_language];
}
// check for english translation
if (array_key_exists('en', $translations)) {
return $translations['en'];
}
// pick the first translation available
if (count($translations) > 0) {
$languages = array_keys($translations);
return $translations[$languages[0]];
}
// we don't have anything to return
throw new \Exception('Nothing to return from translation.');
}
/**
* Translate the name of an attribute.
*
* @param string $name The attribute name.
*
* @return string The translated attribute name, or the original attribute name if no translation was found.
*/
public function getAttributeTranslation($name)
{
// normalize attribute name
$normName = strtolower($name);
$normName = str_replace(":", "_", $normName);
// check for an extra dictionary
$extraDict = $this->configuration->getString('attributes.extradictionary', null);
if ($extraDict !== null) {
$dict = $this->getDictionary($extraDict);
if (array_key_exists($normName, $dict)) {
return $this->getPreferredTranslation($dict[$normName]);
}
}
// search the default attribute dictionary
$dict = $this->getDictionary('attributes');
if (array_key_exists('attribute_'.$normName, $dict)) {
return $this->getPreferredTranslation($dict['attribute_'.$normName]);
}
// no translations found
return $name;
}
/**
* Mark a string for translation without translating it.
*
* @param string $tag A tag name to mark for translation.
*
* @return string The tag, unchanged.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Translate a tag into the current language, with a fallback to english.
*
* This function is used to look up a translation tag in dictionaries, and return the translation into the current
* language. If no translation into the current language can be found, english will be tried, and if that fails,
* placeholder text will be returned.
*
* An array can be passed as the tag. In that case, the array will be assumed to be on the form (language => text),
* and will be used as the source of translations.
*
* This function can also do replacements into the translated tag. It will search the translated tag for the keys
* provided in $replacements, and replace any found occurrences with the value of the key.
*
* @param string|array $tag A tag name for the translation which should be looked up, or an array with
* (language => text) mappings. The array version will go away in 2.0
* @param array $replacements An associative array of keys that should be replaced with values in the
* translated string.
* @param boolean $fallbackdefault Default translation to use as a fallback if no valid translation was found.
* @deprecated Not used in twig, gettext
*
* @return string The translated tag, or a placeholder value if the tag wasn't found.
*/
public function t(
$tag,
$replacements = array(),
// TODO: remove this for 2.0. Assume true
$fallbackdefault = true,
// TODO: remove this for 2.0
$oldreplacements = array(),
// TODO: remove this for 2.0
$striptags = false
) {
$backtrace = debug_backtrace();
$where = $backtrace[0]['file'].':'.$backtrace[0]['line'];
if (!$fallbackdefault) {
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at '.$where.
'. This parameter will go away, the fallback will become' .
' identical to the $tag in 2.0.'
);
}
if (!is_array($replacements)) {
// TODO: remove this entire if for 2.0
// old style call to t(...). Print warning to log
\SimpleSAML\Logger::warning(
'Deprecated use of SimpleSAML\Locale\Translate::t(...) at '.$where.
'. Please update the code to use the new style of parameters.'
);
// for backwards compatibility
if (!$replacements && $this->getTag($tag) === null) {
\SimpleSAML\Logger::warning(
'Code which uses $fallbackdefault === FALSE should be updated to use the getTag() method instead.'
);
return null;
}
$replacements = $oldreplacements;
}
if (is_array($tag)) {
$tagData = $tag;
\SimpleSAML\Logger::warning(
'Deprecated use of new SimpleSAML\Locale\Translate::t(...) at '.$where.
'. The $tag-parameter can only be a string in 2.0.'
);
} else {
$tagData = $this->getTag($tag);
if ($tagData === null) {
// tag not found
\SimpleSAML\Logger::info('Template: Looking up ['.$tag.']: not translated at all.');
return $this->getStringNotTranslated($tag, $fallbackdefault);
}
}
$translated = $this->getPreferredTranslation($tagData);
foreach ($replacements as $k => $v) {
// try to translate if no replacement is given
if ($v == null) {
$v = $this->t($k);
}
$translated = str_replace($k, $v, $translated);
}
return $translated;
}
/**
* Return the string that should be used when no translation was found.
*
* @param string $tag A name tag of the string that should be returned.
* @param boolean $fallbacktag If set to true and string was not found in any languages, return the tag itself. If
* false return null.
*
* @return string The string that should be used, or the tag name if $fallbacktag is set to false.
*/
private function getStringNotTranslated($tag, $fallbacktag)
{
if ($fallbacktag) {
return 'not translated ('.$tag.')';
} else {
return $tag;
}
}
/**
* Include a translation inline instead of putting translations in dictionaries. This function is recommended to be
* used ONLU from variable data, or when the translation is already provided by an external source, as a database
* or in metadata.
*
* @param string $tag The tag that has a translation
* @param array|string $translation The translation array
*
* @throws \Exception If $translation is neither a string nor an array.
*/
public function includeInlineTranslation($tag, $translation)
{
if (is_string($translation)) {
$translation = array('en' => $translation);
} elseif (!is_array($translation)) {
throw new \Exception("Inline translation should be string or array. Is ".gettype($translation)." now!");
}
\SimpleSAML\Logger::debug('Template: Adding inline language translation for tag ['.$tag.']');
$this->langtext[$tag] = $translation;
}
/**
* Include a language file from the dictionaries directory.
*
* @param string $file File name of dictionary to include
* @param \SimpleSAML_Configuration|null $otherConfig Optionally provide a different configuration object than the
* one provided in the constructor to be used to find the directory of the dictionary. This allows to combine
* dictionaries inside the SimpleSAMLphp main code distribution together with external dictionaries. Defaults to
* null.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
if (!empty($otherConfig)) {
$filebase = $otherConfig->getPathValue('dictionarydir', 'dictionaries/');
} else {
$filebase = $this->configuration->getPathValue('dictionarydir', 'dictionaries/');
}
$lang = $this->readDictionaryFile($filebase.$file);
\SimpleSAML\Logger::debug('Template: Merging language array. Loading ['.$file.']');
$this->langtext = array_merge($this->langtext, $lang);
}
/**
* Read a dictionary file in JSON format.
*
* @param string $filename The absolute path to the dictionary file, minus the .definition.json ending.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryJSON($filename)
{
$definitionFile = $filename.'.definition.json';
assert(file_exists($definitionFile));
$fileContent = file_get_contents($definitionFile);
$lang = json_decode($fileContent, true);
if (empty($lang)) {
\SimpleSAML\Logger::error('Invalid dictionary definition file ['.$definitionFile.']');
return array();
}
$translationFile = $filename.'.translation.json';
if (file_exists($translationFile)) {
$fileContent = file_get_contents($translationFile);
$moreTrans = json_decode($fileContent, true);
if (!empty($moreTrans)) {
$lang = array_merge_recursive($lang, $moreTrans);
}
}
return $lang;
}
/**
* Read a dictionary file in PHP format.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryPHP($filename)
{
$phpFile = $filename.'.php';
assert(file_exists($phpFile));
$lang = null;
include($phpFile);
if (isset($lang)) {
return $lang;
}
return array();
}
/**
* Read a dictionary file.
*
* @param string $filename The absolute path to the dictionary file.
*
* @return array An array holding all the translations in the file.
*/
private function readDictionaryFile($filename)
{
assert(is_string($filename));
\SimpleSAML\Logger::debug('Template: Reading ['.$filename.']');
$jsonFile = $filename.'.definition.json';
if (file_exists($jsonFile)) {
return $this->readDictionaryJSON($filename);
}
$phpFile = $filename.'.php';
if (file_exists($phpFile)) {
return $this->readDictionaryPHP($filename);
}
\SimpleSAML\Logger::error(
$_SERVER['PHP_SELF'].' - Template: Could not find dictionary file at ['.$filename.']'
);
return array();
}
public static function translateSingularGettext($original)
{
$text = \Gettext\BaseTranslator::$current->gettext($original);
if (func_num_args() === 1) {
return $text;
}
$args = array_slice(func_get_args(), 1);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
public static function translatePluralGettext($original, $plural, $value)
{
$text = \Gettext\BaseTranslator::$current->ngettext($original, $plural, $value);
if (func_num_args() === 3) {
return $text;
}
$args = array_slice(func_get_args(), 3);
return strtr($text, is_array($args[0]) ? $args[0] : $args);
}
/**
* Pick a translation from a given array of translations for the current language.
*
* @param array $context An array of options. The current language must be specified as an ISO 639 code accessible
* with the key "currentLanguage" in the array.
* @param array $translations An array of translations. Each translation has an ISO 639 code as its key, identifying
* the language it corresponds to.
*
* @return null|string The translation appropriate for the current language, or null if none found. If the
* $context or $translations arrays are null, or $context['currentLanguage'] is not defined, null is also returned.
*/
public static function translateFromArray($context, $translations)
{
if (!is_array($translations) || $translations === null) {
return null;
}
if (!is_array($context) || !isset($context['currentLanguage'])) {
return null;
}
if (isset($translations[$context['currentLanguage']])) {
return $translations[$context['currentLanguage']];
}
// we don't have a translation for the current language, load alternative priorities
$sspcfg = \SimpleSAML_Configuration::getInstance();
$langcfg = $sspcfg->getConfigItem('language', null);
$priorities = array();
if ($langcfg instanceof \SimpleSAML_Configuration) {
$priorities = $langcfg->getArray('priorities', array());
}
foreach ($priorities[$context['currentLanguage']] as $lang) {
if (isset($translations[$lang])) {
return $translations[$lang];
}
}
// nothing we can use, return null so that we can set a default
return null;
}
}

457
lib/SimpleSAML/Logger.php Executable file
View File

@@ -0,0 +1,457 @@
<?php
namespace SimpleSAML;
/**
* The main logger class for SimpleSAMLphp.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class Logger
{
/**
* @var \SimpleSAML\Logger\LoggingHandlerInterface|false|null
*/
private static $loggingHandler = null;
/**
* @var integer|null
*/
private static $logLevel = null;
/**
* @var boolean
*/
private static $captureLog = false;
/**
* @var array
*/
private static $capturedLog = array();
/**
* Array with messages logged before the logging handler was initialized.
*
* @var array
*/
private static $earlyLog = array();
/**
* List of log levels.
*
* This list is used to restore the log levels after some log levels have been disabled.
*
* @var array
*/
private static $logLevelStack = array();
/**
* The current mask of log levels disabled.
*
* Note: this mask is not directly related to the PHP error reporting level.
*
* @var int
*/
private static $logMask = 0;
/**
* This constant defines the string we set the track ID to while we are fetching the track ID from the session
* class. This is used to prevent infinite recursion.
*/
const NO_TRACKID = '_NOTRACKIDYET_';
/**
* This variable holds the track ID we have retrieved from the session class. It can also be NULL, in which case
* we haven't fetched the track ID yet, or self::NO_TRACKID, which means that we are fetching the track ID now.
*/
private static $trackid = self::NO_TRACKID;
/**
* This variable holds the format used to log any message. Its use varies depending on the log handler used (for
* instance, you cannot control here how dates are displayed when using syslog or errorlog handlers), but in
* general the options are:
*
* - %date{<format>}: the date and time, with its format specified inside the brackets. See the PHP documentation
* of the strftime() function for more information on the format. If the brackets are omitted, the standard
* format is applied. This can be useful if you just want to control the placement of the date, but don't care
* about the format.
*
* - %process: the name of the SimpleSAMLphp process. Remember you can configure this in the 'logging.processname'
* option. The SyslogLoggingHandler will just remove this.
*
* - %level: the log level (name or number depending on the handler used). Please note different logging handlers
* will print the log level differently.
*
* - %stat: if the log entry is intended for statistical purposes, it will print the string 'STAT ' (bear in mind
* the trailing space).
*
* - %trackid: the track ID, an identifier that allows you to track a single session.
*
* - %srcip: the IP address of the client. If you are behind a proxy, make sure to modify the
* $_SERVER['REMOTE_ADDR'] variable on your code accordingly to the X-Forwarded-For header.
*
* - %msg: the message to be logged.
*
* @var string The format of the log line.
*/
private static $format = '%date{%b %d %H:%M:%S} %process %level %stat[%trackid] %msg';
/**
* This variable tells if we have a shutdown function registered or not.
*
* @var bool
*/
private static $shutdownRegistered = false;
/**
* This variable tells if we are shutting down.
*
* @var bool
*/
private static $shuttingDown = false;
const EMERG = 0;
const ALERT = 1;
const CRIT = 2;
const ERR = 3;
const WARNING = 4;
const NOTICE = 5;
const INFO = 6;
const DEBUG = 7;
/**
* Log an emergency message.
*
* @var string $string The message to log.
*/
public static function emergency($string)
{
self::log(self::EMERG, $string);
}
/**
* Log a critical message.
*
* @var string $string The message to log.
*/
public static function critical($string)
{
self::log(self::CRIT, $string);
}
/**
* Log an alert.
*
* @var string $string The message to log.
*/
public static function alert($string)
{
self::log(self::ALERT, $string);
}
/**
* Log an error.
*
* @var string $string The message to log.
*/
public static function error($string)
{
self::log(self::ERR, $string);
}
/**
* Log a warning.
*
* @var string $string The message to log.
*/
public static function warning($string)
{
self::log(self::WARNING, $string);
}
/**
* We reserve the notice level for statistics, so do not use this level for other kind of log messages.
*
* @var string $string The message to log.
*/
public static function notice($string)
{
self::log(self::NOTICE, $string);
}
/**
* Info messages are a bit less verbose than debug messages. This is useful to trace a session.
*
* @var string $string The message to log.
*/
public static function info($string)
{
self::log(self::INFO, $string);
}
/**
* Debug messages are very verbose, and will contain more information than what is necessary for a production
* system.
*
* @var string $string The message to log.
*/
public static function debug($string)
{
self::log(self::DEBUG, $string);
}
/**
* Statistics.
*
* @var string $string The message to log.
*/
public static function stats($string)
{
self::log(self::NOTICE, $string, true);
}
/**
* Set the logger to capture logs.
*
* @var boolean $val Whether to capture logs or not. Defaults to TRUE.
*/
public static function setCaptureLog($val = true)
{
self::$captureLog = $val;
}
/**
* Get the captured log.
*/
public static function getCapturedLog()
{
return self::$capturedLog;
}
/**
* Set the track identifier to use in all logs.
*
* @param $trackId string The track identifier to use during this session.
*/
public static function setTrackId($trackId)
{
self::$trackid = $trackId;
}
/**
* Flush any pending log messages to the logging handler.
*
* This method is intended to be registered as a shutdown handler, so that any pending messages that weren't sent
* to the logging handler at that point, can still make it. It is therefore not intended to be called manually.
*
*/
public static function flush()
{
try {
$s = \SimpleSAML_Session::getSessionFromRequest();
} catch (\Exception $e) {
// loading session failed. We don't care why, at this point we have a transient session, so we use that
self::error('Cannot load or create session: '.$e->getMessage());
$s = \SimpleSAML_Session::getSessionFromRequest();
}
self::$trackid = $s->getTrackID();
self::$shuttingDown = true;
foreach (self::$earlyLog as $msg) {
self::log($msg['level'], $msg['string'], $msg['statsLog']);
}
}
/**
* Evaluate whether errors of a certain error level are masked or not.
*
* @param int $errno The level of the error to check.
*
* @return bool True if the error is masked, false otherwise.
*/
public static function isErrorMasked($errno)
{
return ($errno & self::$logMask) || !($errno & error_reporting());
}
/**
* Disable error reporting for the given log levels.
*
* Every call to this function must be followed by a call to popErrorMask().
*
* @param int $mask The log levels that should be masked.
*/
public static function maskErrors($mask)
{
assert(is_int($mask));
$currentEnabled = error_reporting();
self::$logLevelStack[] = array($currentEnabled, self::$logMask);
$currentEnabled &= ~$mask;
error_reporting($currentEnabled);
self::$logMask |= $mask;
}
/**
* Pop an error mask.
*
* This function restores the previous error mask.
*/
public static function popErrorMask()
{
$lastMask = array_pop(self::$logLevelStack);
error_reporting($lastMask[0]);
self::$logMask = $lastMask[1];
}
/**
* Defer a message for later logging.
*
* @param int $level The log level corresponding to this message.
* @param string $message The message itself to log.
* @param boolean $stats Whether this is a stats message or a regular one.
*/
private static function defer($level, $message, $stats)
{
// save the message for later
self::$earlyLog[] = array('level' => $level, 'string' => $message, 'statsLog' => $stats);
// register a shutdown handler if needed
if (!self::$shutdownRegistered) {
register_shutdown_function(array('SimpleSAML\Logger', 'flush'));
self::$shutdownRegistered = true;
}
}
private static function createLoggingHandler($handler = null)
{
// set to false to indicate that it is being initialized
self::$loggingHandler = false;
// a set of known logging handlers
$known_handlers = array(
'syslog' => 'SimpleSAML\Logger\SyslogLoggingHandler',
'file' => 'SimpleSAML\Logger\FileLoggingHandler',
'errorlog' => 'SimpleSAML\Logger\ErrorLogLoggingHandler',
);
// get the configuration
$config = \SimpleSAML_Configuration::getInstance();
assert($config instanceof \SimpleSAML_Configuration);
// setting minimum log_level
self::$logLevel = $config->getInteger('logging.level', self::INFO);
// get the metadata handler option from the configuration
if (is_null($handler)) {
$handler = $config->getString('logging.handler', 'syslog');
}
if (!array_key_exists($handler, $known_handlers) && class_exists($handler)) {
if (!in_array('SimpleSAML\Logger\LoggingHandlerInterface', class_implements($handler), true)) {
throw new \Exception("The logging handler '$handler' is invalid.");
}
} else {
$handler = strtolower($handler);
if (!array_key_exists($handler, $known_handlers)) {
throw new \Exception(
"Invalid value for the 'logging.handler' configuration option. Unknown handler '".$handler."''."
);
}
$handler = $known_handlers[$handler];
}
self::$loggingHandler = new $handler($config);
self::$format = $config->getString('logging.format', self::$format);
self::$loggingHandler->setLogFormat(self::$format);
}
private static function log($level, $string, $statsLog = false)
{
if (self::$loggingHandler === false) {
// some error occurred while initializing logging
self::defer($level, $string, $statsLog);
return;
} elseif (php_sapi_name() === 'cli' || defined('STDIN')) {
// we are being executed from the CLI, nowhere to log
if (is_null(self::$loggingHandler)) {
self::createLoggingHandler('SimpleSAML\Logger\StandardErrorLoggingHandler');
}
$_SERVER['REMOTE_ADDR'] = "CLI";
if (self::$trackid === self::NO_TRACKID) {
self::$trackid = 'CL'.bin2hex(openssl_random_pseudo_bytes(4));
}
} elseif (self::$loggingHandler === null) {
// Initialize logging
self::createLoggingHandler();
if (!empty(self::$earlyLog)) {
// output messages which were logged before we properly initialized logging
foreach (self::$earlyLog as $msg) {
self::log($msg['level'], $msg['string'], $msg['statsLog']);
}
}
}
if (self::$captureLog) {
$ts = microtime(true);
$msecs = (int) (($ts - (int) $ts) * 1000);
$ts = gmdate('H:i:s', $ts).sprintf('.%03d', $msecs).'Z';
self::$capturedLog[] = $ts.' '.$string;
}
if (self::$logLevel >= $level || $statsLog) {
if (is_array($string)) {
$string = implode(",", $string);
}
$formats = array('%trackid', '%msg', '%srcip', '%stat');
$replacements = array(self::$trackid, $string, $_SERVER['REMOTE_ADDR']);
$stat = '';
if ($statsLog) {
$stat = 'STAT ';
}
array_push($replacements, $stat);
if (self::$trackid === self::NO_TRACKID && !self::$shuttingDown) {
// we have a log without track ID and we are not still shutting down, so defer logging
self::defer($level, $string, $statsLog);
return;
} elseif (self::$trackid === self::NO_TRACKID) {
// shutting down without a track ID, prettify it
array_shift($replacements);
array_unshift($replacements, 'N/A');
}
// we either have a track ID or we are shutting down, so just log the message
$string = str_replace($formats, $replacements, self::$format);
self::$loggingHandler->log($level, $string);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Logger;
/**
* A class for logging to the default php error log.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class ErrorLogLoggingHandler implements LoggingHandlerInterface
{
/**
* This array contains the mappings from syslog log level to names.
*/
private static $levelNames = array(
Logger::EMERG => 'EMERG',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRIT',
Logger::ERR => 'ERR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
);
/**
* The name of this process.
*
* @var string
*/
private $processname;
/**
* ErrorLogLoggingHandler constructor.
*
* @param \SimpleSAML_Configuration $config The configuration object for this handler.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
// we don't need the format here
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
} else {
$levelName = sprintf('UNKNOWN%d', $level);
}
$formats = array('%process', '%level');
$replacements = array($this->processname, $levelName);
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
error_log($string);
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Logger;
/**
* A logging handler that dumps logs to files.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class FileLoggingHandler implements LoggingHandlerInterface
{
/**
* A string with the path to the file where we should log our messages.
*
* @var null|string
*/
protected $logFile = null;
/**
* This array contains the mappings from syslog log levels to names. Copied more or less directly from
* SimpleSAML\Logger\ErrorLogLoggingHandler.
*/
private static $levelNames = array(
Logger::EMERG => 'EMERGENCY',
Logger::ALERT => 'ALERT',
Logger::CRIT => 'CRITICAL',
Logger::ERR => 'ERROR',
Logger::WARNING => 'WARNING',
Logger::NOTICE => 'NOTICE',
Logger::INFO => 'INFO',
Logger::DEBUG => 'DEBUG',
);
protected $processname = null;
protected $format;
/**
* Build a new logging handler based on files.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
// get the metadata handler option from the configuration
$this->logFile = $config->getPathValue('loggingdir', 'log/').
$config->getString('logging.logfile', 'simplesamlphp.log');
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
if (@file_exists($this->logFile)) {
if (!@is_writeable($this->logFile)) {
throw new \Exception("Could not write to logfile: ".$this->logFile);
}
} else {
if (!@touch($this->logFile)) {
throw new \Exception(
"Could not create logfile: ".$this->logFile.
" The logging directory is not writable for the web server user."
);
}
}
\SimpleSAML\Utils\Time::initTimezone();
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to the log file.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
if (!is_null($this->logFile)) {
// set human-readable log level. Copied from SimpleSAML\Logger\ErrorLogLoggingHandler.
$levelName = sprintf('UNKNOWN%d', $level);
if (array_key_exists($level, self::$levelNames)) {
$levelName = self::$levelNames[$level];
}
$formats = array('%process', '%level');
$replacements = array($this->processname, $levelName);
$matches = array();
if (preg_match('/%date(?:\{([^\}]+)\})?/', $this->format, $matches)) {
$format = "%b %d %H:%M:%S";
if (isset($matches[1])) {
$format = $matches[1];
}
array_push($formats, $matches[0]);
array_push($replacements, strftime($format));
}
$string = str_replace($formats, $replacements, $string);
file_put_contents($this->logFile, $string.PHP_EOL, FILE_APPEND);
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace SimpleSAML\Logger;
/**
* The interface that must be implemented by any log handler.
*
* @author Jaime Perez Crespo, UNINETT AS.
* @package SimpleSAMLphp
*/
interface LoggingHandlerInterface
{
/**
* Constructor for log handlers. It must accept receiving a \SimpleSAML_Configuration object.
*
* @param \SimpleSAML_Configuration $config The configuration to use in this log handler.
*/
public function __construct(\SimpleSAML_Configuration $config);
/**
* Log a message to its destination.
*
* @param int $level The log level.
* @param string $string The message to log.
*/
public function log($level, $string);
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace SimpleSAML\Logger;
/**
* A logging handler that outputs all messages to standard error.
*
* @author Jaime Perez Crespo, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAMLphp
*/
class StandardErrorLoggingHandler extends FileLoggingHandler
{
/**
* StandardError constructor.
*
* It runs the parent constructor and sets the log file to be the standard error descriptor.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$this->processname = $config->getString('logging.processname', 'SimpleSAMLphp');
$this->logFile = 'php://stderr';
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace SimpleSAML\Logger;
use SimpleSAML\Utils\System;
/**
* A logger that sends messages to syslog.
*
* @author Lasse Birnbaum Jensen, SDU.
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SyslogLoggingHandler implements LoggingHandlerInterface
{
private $isWindows = false;
private $format;
/**
* Build a new logging handler based on syslog.
*/
public function __construct(\SimpleSAML_Configuration $config)
{
$facility = $config->getInteger('logging.facility', defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER);
$processname = $config->getString('logging.processname', 'SimpleSAMLphp');
// Setting facility to LOG_USER (only valid in Windows), enable log level rewrite on windows systems
if (System::getOS() === System::WINDOWS) {
$this->isWindows = true;
$facility = LOG_USER;
}
openlog($processname, LOG_PID, $facility);
}
/**
* Set the format desired for the logs.
*
* @param string $format The format used for logs.
*/
public function setLogFormat($format)
{
$this->format = $format;
}
/**
* Log a message to syslog.
*
* @param int $level The log level.
* @param string $string The formatted message to log.
*/
public function log($level, $string)
{
// changing log level to supported levels if OS is Windows
if ($this->isWindows) {
if ($level <= 4) {
$level = LOG_ERR;
} else {
$level = LOG_INFO;
}
}
$formats = array('%process', '%level');
$replacements = array('', $level);
$string = str_replace($formats, $replacements, $string);
$string = preg_replace('/%\w+(\{[^\}]+\})?/', '', $string);
$string = trim($string);
syslog($level, $string);
}
}

492
lib/SimpleSAML/Memcache.php Executable file
View File

@@ -0,0 +1,492 @@
<?php
/**
* This file implements functions to read and write to a group of memcache
* servers.
*
* The goals of this storage class is to provide failover, redudancy and load
* balancing. This is accomplished by storing the data object to several
* groups of memcache servers. Each data object is replicated to every group
* of memcache servers, but it is only stored to one server in each group.
*
* For this code to work correctly, all web servers accessing the data must
* have the same clock (as measured by the time()-function). Different clock
* values will lead to incorrect behaviour.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Memcache
{
/**
* Cache of the memcache servers we are using.
*
* @var Memcache[]|null
*/
private static $serverGroups = null;
/**
* The flavor of memcache PHP extension we are using.
*
* @var string
*/
private static $extension = '';
/**
* Find data stored with a given key.
*
* @param string $key The key of the data.
*
* @return mixed The data stored with the given key, or null if no data matching the key was found.
*/
public static function get($key)
{
SimpleSAML\Logger::debug("loading key $key from memcache");
$latestInfo = null;
$latestTime = 0.0;
$latestData = null;
$mustUpdate = false;
$allDown = true;
// search all the servers for the given id
foreach (self::getMemcacheServers() as $server) {
$serializedInfo = $server->get($key);
if ($serializedInfo === false) {
// either the server is down, or we don't have the value stored on that server
$mustUpdate = true;
$up = $server->getstats();
if ($up !== false) {
$allDown = false;
}
continue;
}
$allDown = false;
// unserialize the object
$info = unserialize($serializedInfo);
/*
* Make sure that this is an array with two keys:
* - 'timestamp': The time the data was saved.
* - 'data': The data.
*/
if (!is_array($info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Data was not an array.'
);
continue;
}
if (!array_key_exists('timestamp', $info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Missing timestamp.'
);
continue;
}
if (!array_key_exists('data', $info)) {
SimpleSAML\Logger::warning(
'Retrieved invalid data from a memcache server. Missing data.'
);
continue;
}
if ($latestInfo === null) {
// first info found
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
continue;
}
if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) {
// this data matches the data from the other server(s)
continue;
}
// different data from different servers. We need to update at least one of them to maintain sync
$mustUpdate = true;
// update if data in $info is newer than $latestData
if ($latestTime < $info['timestamp']) {
$latestInfo = $serializedInfo;
$latestTime = $info['timestamp'];
$latestData = $info['data'];
}
}
if ($latestData === null) {
if ($allDown) {
// all servers are down, panic!
$e = new SimpleSAML_Error_Error('MEMCACHEDOWN', null, 503);
throw new SimpleSAML_Error_Exception('All memcache servers are down', 503, $e);
}
// we didn't find any data matching the key
SimpleSAML\Logger::debug("key $key not found in memcache");
return null;
}
if ($mustUpdate) {
// we found data matching the key, but some of the servers need updating
SimpleSAML\Logger::debug("Memcache servers out of sync for $key, forcing sync");
self::set($key, $latestData);
}
return $latestData;
}
/**
* Save a key-value pair to the memcache servers.
*
* @param string $key The key of the data.
* @param mixed $value The value of the data.
* @param integer|null $expire The expiration timestamp of the data.
*/
public static function set($key, $value, $expire = null)
{
SimpleSAML\Logger::debug("saving key $key to memcache");
$savedInfo = array(
'timestamp' => microtime(true),
'data' => $value
);
if ($expire === null) {
$expire = self::getExpireTime();
}
$savedInfoSerialized = serialize($savedInfo);
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
if (self::$extension === 'memcached') {
$server->set($key, $savedInfoSerialized, $expire);
} else {
$server->set($key, $savedInfoSerialized, 0, $expire);
}
}
}
/**
* Delete a key-value pair from the memcache servers.
*
* @param string $key The key we should delete.
*/
public static function delete($key)
{
assert(is_string($key));
SimpleSAML\Logger::debug("deleting key $key from memcache");
// store this object to all groups of memcache servers
foreach (self::getMemcacheServers() as $server) {
$server->delete($key);
}
}
/**
* This function adds a server from the 'memcache_store.servers'
* configuration option to a Memcache object.
*
* The server parameter is an array with the following keys:
* - hostname
* Hostname or ip address to the memcache server.
* - port (optional)
* port number the memcache server is running on. This
* defaults to memcache.default_port if no value is given.
* The default value of memcache.default_port is 11211.
* - weight (optional)
* The weight of this server in the load balancing
* cluster.
* - timeout (optional)
* The timeout for contacting this server, in seconds.
* The default value is 3 seconds.
*
* @param Memcache $memcache The Memcache object we should add this server to.
* @param array $server An associative array with the configuration options for the server to add.
*
* @throws Exception If any configuration option for the server is invalid.
*/
private static function addMemcacheServer($memcache, $server)
{
// the hostname option is required
if (!array_key_exists('hostname', $server)) {
throw new Exception(
"hostname setting missing from server in the 'memcache_store.servers' configuration option."
);
}
$hostname = $server['hostname'];
// the hostname must be a valid string
if (!is_string($hostname)) {
throw new Exception(
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is".
' supposed to be a string.'
);
}
// check if we are told to use a socket
$socket = false;
if (strpos($hostname, 'unix:///') === 0) {
$socket = true;
}
// check if the user has specified a port number
if ($socket) {
// force port to be 0 for sockets
$port = 0;
} elseif (array_key_exists('port', $server)) {
// get the port number from the array, and validate it
$port = (int) $server['port'];
if (($port <= 0) || ($port > 65535)) {
throw new Exception(
"Invalid port for server in the 'memcache_store.servers' configuration option. The port number".
' is supposed to be an integer between 0 and 65535.'
);
}
} else {
// use the default port number from the ini-file
$port = (int) ini_get('memcache.default_port');
if ($port <= 0 || $port > 65535) {
// invalid port number from the ini-file. fall back to the default
$port = 11211;
}
}
// check if the user has specified a weight for this server
if (array_key_exists('weight', $server)) {
// get the weight and validate it
$weight = (int) $server['weight'];
if ($weight <= 0) {
throw new Exception(
"Invalid weight for server in the 'memcache_store.servers' configuration option. The weight is".
' supposed to be a positive integer.'
);
}
} else {
// use a default weight of 1
$weight = 1;
}
// check if the user has specified a timeout for this server
if (array_key_exists('timeout', $server)) {
// get the timeout and validate it
$timeout = (int) $server['timeout'];
if ($timeout <= 0) {
throw new Exception(
"Invalid timeout for server in the 'memcache_store.servers' configuration option. The timeout is".
' supposed to be a positive integer.'
);
}
} else {
// use a default timeout of 3 seconds
$timeout = 3;
}
// add this server to the Memcache object
if (self::$extension === 'memcached') {
$memcache->addServer($hostname, $port);
} else {
$memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
}
}
/**
* This function takes in a list of servers belonging to a group and
* creates a Memcache object from the servers in the group.
*
* @param array $group Array of servers which should be created as a group.
*
* @return Memcache A Memcache object of the servers in the group
*
* @throws Exception If the servers configuration is invalid.
*/
private static function loadMemcacheServerGroup(array $group)
{
$class = class_exists('Memcache') ? 'Memcache' : (class_exists('Memcached') ? 'Memcached' : false);
if (!$class) {
throw new Exception('Missing Memcached implementation. You must install either the Memcache or Memcached extension.');
}
self::$extension = strtolower($class);
// create the Memcache object
$memcache = new $class();
// iterate over all the servers in the group and add them to the Memcache object
foreach ($group as $index => $server) {
// make sure that we don't have an index. An index would be a sign of invalid configuration
if (!is_int($index)) {
throw new Exception(
"Invalid index on element in the 'memcache_store.servers' configuration option. Perhaps you".
' have forgotten to add an array(...) around one of the server groups? The invalid index was: '.
$index
);
}
// make sure that the server object is an array. Each server is an array with name-value pairs
if (!is_array($server)) {
throw new Exception(
'Invalid value for the server with index '.$index.
'. Remeber that the \'memcache_store.servers\' configuration option'.
' contains an array of arrays of arrays.'
);
}
self::addMemcacheServer($memcache, $server);
}
return $memcache;
}
/**
* This function gets a list of all configured memcache servers. This list is initialized based
* on the content of 'memcache_store.servers' in the configuration.
*
* @return Memcache[] Array with Memcache objects.
*
* @throws Exception If the servers configuration is invalid.
*/
private static function getMemcacheServers()
{
// check if we have loaded the servers already
if (self::$serverGroups != null) {
return self::$serverGroups;
}
// initialize the servers-array
self::$serverGroups = array();
// load the configuration
$config = SimpleSAML_Configuration::getInstance();
$groups = $config->getArray('memcache_store.servers');
// iterate over all the groups in the 'memcache_store.servers' configuration option
foreach ($groups as $index => $group) {
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
if (!is_int($index)) {
throw new Exception(
"Invalid index on element in the 'memcache_store.servers'".
' configuration option. Perhaps you have forgotten to add an array(...)'.
' around one of the server groups? The invalid index was: '.$index
);
}
/*
* Make sure that the group is an array. Each group is an array of servers. Each server is
* an array of name => value pairs for that server.
*/
if (!is_array($group)) {
throw new Exception(
"Invalid value for the server with index ".$index.
". Remeber that the 'memcache_store.servers' configuration option".
' contains an array of arrays of arrays.'
);
}
// parse and add this group to the server group list
self::$serverGroups[] = self::loadMemcacheServerGroup($group);
}
return self::$serverGroups;
}
/**
* This is a helper-function which returns the expire value of data
* we should store to the memcache servers.
*
* The value is set depending on the configuration. If no value is
* set in the configuration, then we will use a default value of 0.
* 0 means that the item will never expire.
*
* @return integer The value which should be passed in the set(...) calls to the memcache objects.
*
* @throws Exception If the option 'memcache_store.expires' has a negative value.
*/
private static function getExpireTime()
{
// get the configuration instance
$config = SimpleSAML_Configuration::getInstance();
assert($config instanceof SimpleSAML_Configuration);
// get the expire-value from the configuration
$expire = $config->getInteger('memcache_store.expires', 0);
// it must be a positive integer
if ($expire < 0) {
throw new Exception(
"The value of 'memcache_store.expires' in the configuration can't be a negative integer."
);
}
/* If the configuration option is 0, then we should return 0. This allows the user to specify that the data
* shouldn't expire.
*/
if ($expire == 0) {
return 0;
}
/* The expire option is given as the number of seconds into the future an item should expire. We convert this
* to an actual timestamp.
*/
$expireTime = time() + $expire;
return $expireTime;
}
/**
* This function retrieves statistics about all memcache server groups.
*
* @return array Array with the names of each stat and an array with the value for each server group.
*
* @throws Exception If memcache server status couldn't be retrieved.
*/
public static function getStats()
{
$ret = array();
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
foreach ($stats as $server => $data) {
if ($data === false) {
throw new Exception('Failed to get memcache server status.');
}
}
$stats = SimpleSAML\Utils\Arrays::transpose($stats);
$ret = array_merge_recursive($ret, $stats);
}
return $ret;
}
/**
* Retrieve statistics directly in the form returned by getExtendedStats, for
* all server groups.
*
* @return array An array with the extended stats output for each server group.
*/
public static function getRawStats()
{
$ret = array();
foreach (self::getMemcacheServers() as $sg) {
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
$ret[] = $stats;
}
return $ret;
}
}

View File

@@ -0,0 +1,361 @@
<?php
/**
* This file defines a class for metadata handling.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandler
{
/**
* This static variable contains a reference to the current
* instance of the metadata handler. This variable will be null if
* we haven't instantiated a metadata handler yet.
*
* @var SimpleSAML_Metadata_MetaDataStorageHandler
*/
private static $metadataHandler = null;
/**
* This is a list of all the metadata sources we have in our metadata
* chain. When we need metadata, we will look through this chain from start to end.
*
* @var SimpleSAML_Metadata_MetaDataStorageSource[]
*/
private $sources;
/**
* This function retrieves the current instance of the metadata handler.
* The metadata handler will be instantiated if this is the first call
* to this function.
*
* @return SimpleSAML_Metadata_MetaDataStorageHandler The current metadata handler instance.
*/
public static function getMetadataHandler()
{
if (self::$metadataHandler === null) {
self::$metadataHandler = new SimpleSAML_Metadata_MetaDataStorageHandler();
}
return self::$metadataHandler;
}
/**
* This constructor initializes this metadata storage handler. It will load and
* parse the configuration, and initialize the metadata source list.
*/
protected function __construct()
{
$config = SimpleSAML_Configuration::getInstance();
$sourcesConfig = $config->getArray('metadata.sources', null);
// for backwards compatibility, and to provide a default configuration
if ($sourcesConfig === null) {
$type = $config->getString('metadata.handler', 'flatfile');
$sourcesConfig = array(array('type' => $type));
}
try {
$this->sources = SimpleSAML_Metadata_MetaDataStorageSource::parseSources($sourcesConfig);
} catch (Exception $e) {
throw new Exception(
"Invalid configuration of the 'metadata.sources' configuration option: ".$e->getMessage()
);
}
}
/**
* This function is used to generate some metadata elements automatically.
*
* @param string $property The metadata property which should be auto-generated.
* @param string $set The set we the property comes from.
*
* @return string The auto-generated metadata property.
* @throws Exception If the metadata cannot be generated automatically.
*/
public function getGenerated($property, $set)
{
// first we check if the user has overridden this property in the metadata
try {
$metadataSet = $this->getMetaDataCurrent($set);
if (array_key_exists($property, $metadataSet)) {
return $metadataSet[$property];
}
} catch (Exception $e) {
// probably metadata wasn't found. In any case we continue by generating the metadata
}
// get the configuration
$config = SimpleSAML_Configuration::getInstance();
assert($config instanceof SimpleSAML_Configuration);
$baseurl = \SimpleSAML\Utils\HTTP::getSelfURLHost().$config->getBasePath();
if ($set == 'saml20-sp-hosted') {
if ($property === 'SingleLogoutServiceBinding') {
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'saml20-idp-hosted') {
switch ($property) {
case 'SingleSignOnService':
return $baseurl.'saml2/idp/SSOService.php';
case 'SingleSignOnServiceBinding':
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
case 'SingleLogoutService':
return $baseurl.'saml2/idp/SingleLogoutService.php';
case 'SingleLogoutServiceBinding':
return \SAML2\Constants::BINDING_HTTP_REDIRECT;
}
} elseif ($set == 'shib13-idp-hosted') {
if ($property === 'SingleSignOnService') {
return $baseurl.'shib13/idp/SSOService.php';
}
}
throw new Exception('Could not generate metadata property '.$property.' for set '.$set.'.');
}
/**
* This function lists all known metadata in the given set. It is returned as an associative array
* where the key is the entity id.
*
* @param string $set The set we want to list metadata from.
*
* @return array An associative array with the metadata from from the given set.
*/
public function getList($set = 'saml20-idp-remote')
{
assert(is_string($set));
$result = array();
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
foreach ($srcList as $key => $le) {
if (array_key_exists('expire', $le)) {
if ($le['expire'] < time()) {
unset($srcList[$key]);
SimpleSAML\Logger::warning(
"Dropping metadata entity ".var_export($key, true).", expired ".
SimpleSAML\Utils\Time::generateTimestamp($le['expire'])."."
);
}
}
}
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
return $result;
}
/**
* This function retrieves metadata for the current entity based on the hostname/path the request
* was directed to. It will throw an exception if it is unable to locate the metadata.
*
* @param string $set The set we want metadata from.
*
* @return array An associative array with the metadata.
*/
public function getMetaDataCurrent($set)
{
return $this->getMetaData(null, $set);
}
/**
* This function locates the current entity id based on the hostname/path combination the user accessed.
* It will throw an exception if it is unable to locate the entity id.
*
* @param string $set The set we look for the entity id in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string The entity id which is associated with the current hostname/path combination.
* @throws Exception If no default metadata can be found in the set for the current host.
*/
public function getMetaDataCurrentEntityID($set, $type = 'entityid')
{
assert(is_string($set));
// first we look for the hostname/path combination
$currenthostwithpath = \SimpleSAML\Utils\HTTP::getSelfHostWithPath(); // sp.example.org/university
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthostwithpath, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the hostname
$currenthost = \SimpleSAML\Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($this->sources as $source) {
$index = $source->getEntityIdFromHostPath($currenthost, $set, $type);
if ($index !== null) {
return $index;
}
}
// then we look for the DEFAULT entry
foreach ($this->sources as $source) {
$entityId = $source->getEntityIdFromHostPath('__DEFAULT__', $set, $type);
if ($entityId !== null) {
return $entityId;
}
}
// we were unable to find the hostname/path in any metadata source
throw new Exception(
'Could not find any default metadata entities in set ['.$set.'] for host ['.$currenthost.' : '.
$currenthostwithpath.']'
);
}
/**
* This method will call getPreferredEntityIdFromCIDRhint() on all of the
* sources.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
*
* @return string The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip)
{
foreach ($this->sources as $source) {
$entityId = $source->getPreferredEntityIdFromCIDRhint($set, $ip);
if ($entityId !== null) {
return $entityId;
}
}
return null;
}
/**
* This function looks up the metadata for the given entity id in the given set. It will throw an
* exception if it is unable to locate the metadata.
*
* @param string $index The entity id we are looking up. This parameter may be NULL, in which case we look up
* the current entity id based on the current hostname/path.
* @param string $set The set of metadata we are looking up the entity id in.
*
* @return array The metadata array describing the specified entity.
* @throws Exception If metadata for the specified entity is expired.
* @throws SimpleSAML_Error_MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaData($index, $set)
{
assert(is_string($set));
if ($index === null) {
$index = $this->getMetaDataCurrentEntityID($set, 'metaindex');
}
assert(is_string($index));
foreach ($this->sources as $source) {
$metadata = $source->getMetaData($index, $set);
if ($metadata !== null) {
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] < time()) {
throw new Exception(
'Metadata for the entity ['.$index.'] expired '.
(time() - $metadata['expire']).' seconds ago.'
);
}
}
$metadata['metadata-index'] = $index;
$metadata['metadata-set'] = $set;
assert(array_key_exists('entityid', $metadata));
return $metadata;
}
}
throw new SimpleSAML_Error_MetadataNotFound($index);
}
/**
* Retrieve the metadata as a configuration object.
*
* This function will throw an exception if it is unable to locate the metadata.
*
* @param string $entityId The entity ID we are looking up.
* @param string $set The metadata set we are searching.
*
* @return SimpleSAML_Configuration The configuration object representing the metadata.
* @throws SimpleSAML_Error_MetadataNotFound If no metadata for the entity specified can be found.
*/
public function getMetaDataConfig($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$metadata = $this->getMetaData($entityId, $set);
return SimpleSAML_Configuration::loadFromArray($metadata, $set.'/'.var_export($entityId, true));
}
/**
* Search for an entity's metadata, given the SHA1 digest of its entity ID.
*
* @param string $sha1 The SHA1 digest of the entity ID.
* @param string $set The metadata set we are searching.
*
* @return null|SimpleSAML_Configuration The metadata corresponding to the entity, or null if the entity cannot be
* found.
*/
public function getMetaDataConfigForSha1($sha1, $set)
{
assert(is_string($sha1));
assert(is_string($set));
$result = array();
foreach ($this->sources as $source) {
$srcList = $source->getMetadataSet($set);
/* $result is the last argument to array_merge because we want the content already
* in $result to have precedence.
*/
$result = array_merge($srcList, $result);
}
foreach ($result as $remote_provider) {
if (sha1($remote_provider['entityid']) == $sha1) {
$remote_provider['metadata-set'] = $set;
return SimpleSAML_Configuration::loadFromArray(
$remote_provider,
$set.'/'.var_export($remote_provider['entityid'], true)
);
}
}
return null;
}
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* This file defines a flat file metadata source.
* Instantiation of session handler objects should be done through
* the class method getMetadataHandler().
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* This is the directory we will load metadata files from. The path will always end
* with a '/'.
*
* @var string
*/
private $directory;
/**
* This is an associative array which stores the different metadata sets we have loaded.
*
* @var array
*/
private $cachedMetadata = array();
/**
* This constructor initializes the flatfile metadata storage handler with the
* specified configuration. The configuration is an associative array with the following
* possible elements:
* - 'directory': The directory we should load metadata from. The default directory is
* set in the 'metadatadir' configuration option in 'config.php'.
*
* @param array $config An associative array with the configuration for this handler.
*/
protected function __construct($config)
{
assert(is_array($config));
// get the configuration
$globalConfig = SimpleSAML_Configuration::getInstance();
// find the path to the directory we should search for metadata in
if (array_key_exists('directory', $config)) {
$this->directory = $config['directory'];
} else {
$this->directory = $globalConfig->getString('metadatadir', 'metadata/');
}
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
$this->directory = $globalConfig->resolvePath($this->directory).'/';
}
/**
* This function loads the given set of metadata from a file our metadata directory.
* This function returns null if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array An associative array with the metadata, or null if we are unable to load metadata from the given
* file.
* @throws Exception If the metadata set cannot be loaded.
*/
private function load($set)
{
$metadatasetfile = $this->directory.$set.'.php';
if (!file_exists($metadatasetfile)) {
return null;
}
$metadata = array();
include($metadatasetfile);
if (!is_array($metadata)) {
throw new Exception('Could not load metadata set ['.$set.'] from file: '.$metadatasetfile);
}
return $metadata;
}
/**
* This function retrieves the given set of metadata. It will return an empty array if it is
* unable to locate it.
*
* @param string $set The set of metadata we are retrieving.
*
* @return array An associative array with the metadata. Each element in the array is an entity, and the
* key is the entity id.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = array();
}
// add the entity id of an entry to each entry in the metadata
foreach ($metadataSet as $entityId => &$entry) {
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
} else {
$entry['entityid'] = $entityId;
}
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
private function generateDynamicHostedEntityID($set)
{
// get the configuration
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
if ($set === 'saml20-idp-hosted') {
return $baseurl.'saml2/idp/metadata.php';
} elseif ($set === 'shib13-idp-hosted') {
return $baseurl.'shib13/idp/metadata.php';
} elseif ($set === 'wsfed-sp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
} elseif ($set === 'adfs-idp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
} else {
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
}
}
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* Class for handling metadata files stored in a database.
*
* This class has been based off a previous version written by
* mooknarf@gmail.com and patched to work with the latest version
* of SimpleSAMLphp
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerPdo extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The PDO object
*/
private $db;
/**
* Prefix to apply to the metadata table
*/
private $tablePrefix;
/**
* This is an associative array which stores the different metadata sets we have loaded.
*/
private $cachedMetadata = array();
/**
* All the metadata sets supported by this MetaDataStorageHandler
*/
public $supportedSets = array(
'adfs-idp-hosted',
'adfs-sp-remote',
'saml20-idp-hosted',
'saml20-idp-remote',
'saml20-sp-remote',
'shib13-idp-hosted',
'shib13-idp-remote',
'shib13-sp-hosted',
'shib13-sp-remote',
'wsfed-idp-remote',
'wsfed-sp-hosted'
);
/**
* This constructor initializes the PDO metadata storage handler with the specified
* configuration. The configuration is an associative array with the following
* possible elements (set in config.php):
* - 'usePersistentConnection': TRUE/FALSE if database connection should be persistent.
* - 'dsn': The database connection string.
* - 'username': Database user name
* - 'password': Password for the database user.
*
* @param array $config An associative array with the configuration for this handler.
*/
public function __construct($config)
{
assert(is_array($config));
$this->db = SimpleSAML\Database::getInstance();
}
/**
* This function loads the given set of metadata from a file to a configured database.
* This function returns NULL if it is unable to locate the given set in the metadata directory.
*
* @param string $set The set of metadata we are loading.
*
* @return array $metadata Associative array with the metadata, or NULL if we are unable to load metadata from the
* given file.
*
* @throws Exception If a database error occurs.
* @throws SimpleSAML_Error_Exception If the metadata can be retrieved from the database, but cannot be decoded.
*/
private function load($set)
{
assert(is_string($set));
$tableName = $this->getTableName($set);
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName");
if ($stmt->execute()) {
$metadata = array();
while ($d = $stmt->fetch()) {
$data = json_decode($d['entity_data'], true);
if ($data === null) {
throw new SimpleSAML_Error_Exception("Cannot decode metadata for entity '${d['entity_id']}'");
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $d['entity_id'];
}
$metadata[$d['entity_id']] = $data;
}
return $metadata;
} else {
throw new Exception('PDO metadata handler: Database error: '.var_export($this->db->getLastError(), true));
}
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array $metadata An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
if (array_key_exists($set, $this->cachedMetadata)) {
return $this->cachedMetadata[$set];
}
$metadataSet = $this->load($set);
if ($metadataSet === null) {
$metadataSet = array();
}
foreach ($metadataSet as $entityId => &$entry) {
if (preg_match('/__DYNAMIC(:[0-9]+)?__/', $entityId)) {
$entry['entityid'] = $this->generateDynamicHostedEntityID($set);
} else {
$entry['entityid'] = $entityId;
}
}
$this->cachedMetadata[$set] = $metadataSet;
return $metadataSet;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$tableName = $this->getTableName($set);
if (!in_array($set, $this->supportedSets, true)) {
return null;
}
$stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName WHERE entity_id=:entityId", array('entityId' => $entityId));
if ($stmt->execute()) {
$rowCount = 0;
$data = null;
while ($d = $stmt->fetch()) {
if (++$rowCount > 1) {
SimpleSAML\Logger::warning("Duplicate match for $entityId in set $set");
break;
}
$data = json_decode($d['entity_data'], true);
if ($data === null) {
throw new SimpleSAML_Error_Exception("Cannot decode metadata for entity '${d['entity_id']}'");
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $d['entity_id'];
}
}
return $data;
} else {
throw new Exception('PDO metadata handler: Database error: '.var_export($this->db->getLastError(), true));
}
}
private function generateDynamicHostedEntityID($set)
{
assert(is_string($set));
// get the configuration
$baseurl = \SimpleSAML\Utils\HTTP::getBaseURL();
if ($set === 'saml20-idp-hosted') {
return $baseurl.'saml2/idp/metadata.php';
} elseif ($set === 'saml20-sp-hosted') {
return $baseurl.'saml2/sp/metadata.php';
} elseif ($set === 'shib13-idp-hosted') {
return $baseurl.'shib13/idp/metadata.php';
} elseif ($set === 'shib13-sp-hosted') {
return $baseurl.'shib13/sp/metadata.php';
} elseif ($set === 'wsfed-sp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost();
} elseif ($set === 'adfs-idp-hosted') {
return 'urn:federation:'.\SimpleSAML\Utils\HTTP::getSelfHost().':idp';
} else {
throw new Exception('Can not generate dynamic EntityID for metadata of this type: ['.$set.']');
}
}
/**
* Add metadata to the configured database
*
* @param string $index Entity ID
* @param string $set The set to add the metadata to
* @param array $entityData Metadata
*
* @return bool True/False if entry was successfully added
*/
public function addEntry($index, $set, $entityData)
{
assert(is_string($index));
assert(is_string($set));
assert(is_array($entityData));
if (!in_array($set, $this->supportedSets, true)) {
return false;
}
$tableName = $this->getTableName($set);
$metadata = $this->db->read(
"SELECT entity_id, entity_data FROM $tableName WHERE entity_id = :entity_id",
array(
'entity_id' => $index,
)
);
$retrivedEntityIDs = $metadata->fetch();
$params = array(
'entity_id' => $index,
'entity_data' => json_encode($entityData),
);
if ($retrivedEntityIDs !== false && count($retrivedEntityIDs) > 0) {
$rows = $this->db->write(
"UPDATE $tableName SET entity_data = :entity_data WHERE entity_id = :entity_id",
$params
);
} else {
$rows = $this->db->write(
"INSERT INTO $tableName (entity_id, entity_data) VALUES (:entity_id, :entity_data)",
$params
);
}
return $rows === 1;
}
/**
* Replace the -'s to an _ in table names for Metadata sets
* since SQL does not allow a - in a table name.
*
* @param string $table Table
*
* @return string Replaced table name
*/
private function getTableName($table)
{
assert(is_string($table));
return $this->db->applyPrefix(str_replace("-", "_", $this->tablePrefix.$table));
}
/**
* Initialize the configured database
*
* @return int|false The number of SQL statements successfully executed, false if some error occurred.
*/
public function initDatabase()
{
$stmt = 0;
$fine = true;
foreach ($this->supportedSets as $set) {
$tableName = $this->getTableName($set);
$rows = $this->db->write(
"CREATE TABLE IF NOT EXISTS $tableName (entity_id VARCHAR(255) PRIMARY KEY NOT NULL, entity_data ".
"TEXT NOT NULL)"
);
if ($rows === 0) {
$fine = false;
} else {
$stmt += $rows;
}
}
if (!$fine) {
return false;
}
return $stmt;
}
}

View File

@@ -0,0 +1,286 @@
<?php
/**
* Class for handling metadata files in serialized format.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerSerialize extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The file extension we use for our metadata files.
*
* @var string
*/
const EXTENSION = '.serialized';
/**
* The base directory where metadata is stored.
*
* @var string
*/
private $directory;
/**
* Constructor for this metadata handler.
*
* Parses configuration.
*
* @param array $config The configuration for this metadata handler.
*/
public function __construct($config)
{
assert(is_array($config));
$globalConfig = SimpleSAML_Configuration::getInstance();
$cfgHelp = SimpleSAML_Configuration::loadFromArray($config, 'serialize metadata source');
$this->directory = $cfgHelp->getString('directory');
/* Resolve this directory relative to the SimpleSAMLphp directory (unless it is
* an absolute path).
*/
$this->directory = $globalConfig->resolvePath($this->directory);
}
/**
* Helper function for retrieving the path of a metadata file.
*
* @param string $entityId The entity ID.
* @param string $set The metadata set.
*
* @return string The path to the metadata file.
*/
private function getMetadataPath($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
return $this->directory.'/'.rawurlencode($set).'/'.rawurlencode($entityId).self::EXTENSION;
}
/**
* Retrieve a list of all available metadata sets.
*
* @return array An array with the available sets.
*/
public function getMetadataSets()
{
$ret = array();
$dh = @opendir($this->directory);
if ($dh === false) {
SimpleSAML\Logger::warning(
'Serialize metadata handler: Unable to open directory: '.var_export($this->directory, true)
);
return $ret;
}
while (($entry = readdir($dh)) !== false) {
if ($entry[0] === '.') {
// skip '..', '.' and hidden files
continue;
}
$path = $this->directory.'/'.$entry;
if (!is_dir($path)) {
SimpleSAML\Logger::warning(
'Serialize metadata handler: Metadata directory contained a file where only directories should '.
'exist: '.var_export($path, true)
);
continue;
}
$ret[] = rawurldecode($entry);
}
closedir($dh);
return $ret;
}
/**
* Retrieve a list of all available metadata for a given set.
*
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with all the metadata for the given set.
*/
public function getMetadataSet($set)
{
assert(is_string($set));
$ret = array();
$dir = $this->directory.'/'.rawurlencode($set);
if (!is_dir($dir)) {
// probably some code asked for a metadata set which wasn't available
return $ret;
}
$dh = @opendir($dir);
if ($dh === false) {
SimpleSAML\Logger::warning('Serialize metadata handler: Unable to open directory: '.var_export($dir, true));
return $ret;
}
$extLen = strlen(self::EXTENSION);
while (($file = readdir($dh)) !== false) {
if (strlen($file) <= $extLen) {
continue;
}
if (substr($file, -$extLen) !== self::EXTENSION) {
continue;
}
$entityId = substr($file, 0, -$extLen);
$entityId = rawurldecode($entityId);
$md = $this->getMetaData($entityId, $set);
if ($md !== null) {
$ret[$entityId] = $md;
}
}
closedir($dh);
return $ret;
}
/**
* Retrieve a metadata entry.
*
* @param string $entityId The entityId we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
return null;
}
$data = @file_get_contents($filePath);
if ($data === false) {
$error = error_get_last();
SimpleSAML\Logger::warning(
'Error reading file '.$filePath.': '.$error['message']
);
return null;
}
$data = @unserialize($data);
if ($data === false) {
SimpleSAML\Logger::warning('Error unserializing file: '.$filePath);
return null;
}
if (!array_key_exists('entityid', $data)) {
$data['entityid'] = $entityId;
}
return $data;
}
/**
* Save a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
* @param array $metadata The metadata.
*
* @return boolean True if successfully saved, false otherwise.
*/
public function saveMetadata($entityId, $set, $metadata)
{
assert(is_string($entityId));
assert(is_string($set));
assert(is_array($metadata));
$filePath = $this->getMetadataPath($entityId, $set);
$newPath = $filePath.'.new';
$dir = dirname($filePath);
if (!is_dir($dir)) {
SimpleSAML\Logger::info('Creating directory: '.$dir);
$res = @mkdir($dir, 0777, true);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Failed to create directory '.$dir.': '.$error['message']);
return false;
}
}
$data = serialize($metadata);
SimpleSAML\Logger::debug('Writing: '.$newPath);
$res = file_put_contents($newPath, $data);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Error saving file '.$newPath.': '.$error['message']);
return false;
}
$res = rename($newPath, $filePath);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error('Error renaming '.$newPath.' to '.$filePath.': '.$error['message']);
return false;
}
return true;
}
/**
* Delete a metadata entry.
*
* @param string $entityId The entityId of the metadata entry.
* @param string $set The metadata set this metadata entry belongs to.
*/
public function deleteMetadata($entityId, $set)
{
assert(is_string($entityId));
assert(is_string($set));
$filePath = $this->getMetadataPath($entityId, $set);
if (!file_exists($filePath)) {
SimpleSAML\Logger::warning(
'Attempted to erase nonexistent metadata entry '.
var_export($entityId, true).' in set '.var_export($set, true).'.'
);
return;
}
$res = unlink($filePath);
if ($res === false) {
$error = error_get_last();
SimpleSAML\Logger::error(
'Failed to delete file '.$filePath.
': '.$error['message']
);
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* This class implements a metadata source which loads metadata from XML files.
* The XML files should be in the SAML 2.0 metadata format.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_MetaDataStorageHandlerXML extends SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* This variable contains an associative array with the parsed metadata.
*
* @var array
*/
private $metadata;
/**
* This function initializes the XML metadata source. The configuration must contain one of
* the following options:
* - 'file': Path to a file with the metadata. This path is relative to the SimpleSAMLphp
* base directory.
* - 'url': URL we should download the metadata from. This is only meant for testing.
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws Exception If neither the 'file' or 'url' options are defined in the configuration.
*/
protected function __construct($config)
{
$src = $srcXml = null;
if (array_key_exists('file', $config)) {
// get the configuration
$globalConfig = SimpleSAML_Configuration::getInstance();
$src = $globalConfig->resolvePath($config['file']);
} elseif (array_key_exists('url', $config)) {
$src = $config['url'];
} elseif (array_key_exists('xml', $config)) {
$srcXml = $config['xml'];
} else {
throw new Exception("Missing one of 'file', 'url' and 'xml' in XML metadata source configuration.");
}
$SP1x = array();
$IdP1x = array();
$SP20 = array();
$IdP20 = array();
$AAD = array();
if(isset($src)) {
$entities = SimpleSAML_Metadata_SAMLParser::parseDescriptorsFile($src);
} elseif(isset($srcXml)) {
$entities = SimpleSAML_Metadata_SAMLParser::parseDescriptorsString($srcXml);
} else {
throw new Exception("Neither source file path/URI nor string data provided");
}
foreach ($entities as $entityId => $entity) {
$md = $entity->getMetadata1xSP();
if ($md !== null) {
$SP1x[$entityId] = $md;
}
$md = $entity->getMetadata1xIdP();
if ($md !== null) {
$IdP1x[$entityId] = $md;
}
$md = $entity->getMetadata20SP();
if ($md !== null) {
$SP20[$entityId] = $md;
}
$md = $entity->getMetadata20IdP();
if ($md !== null) {
$IdP20[$entityId] = $md;
}
$md = $entity->getAttributeAuthorities();
if (count($md) > 0) {
$AAD[$entityId] = $md[0];
}
}
$this->metadata = array(
'shib13-sp-remote' => $SP1x,
'shib13-idp-remote' => $IdP1x,
'saml20-sp-remote' => $SP20,
'saml20-idp-remote' => $IdP20,
'attributeauthority-remote' => $AAD,
);
}
/**
* This function returns an associative array with metadata for all entities in the given set. The
* key of the array is the entity id.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set.
*/
public function getMetadataSet($set)
{
if (array_key_exists($set, $this->metadata)) {
return $this->metadata[$set];
}
// we don't have this metadata set
return array();
}
}

View File

@@ -0,0 +1,275 @@
<?php
/**
* This abstract class defines an interface for metadata storage sources.
*
* It also contains the overview of the different metadata storage sources.
* A metadata storage source can be loaded by passing the configuration of it
* to the getSource static function.
*
* @author Olav Morken, UNINETT AS.
* @author Andreas Aakre Solberg, UNINETT AS.
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* Parse array with metadata sources.
*
* This function accepts an array with metadata sources, and returns an array with
* each metadata source as an object.
*
* @param array $sourcesConfig Array with metadata source configuration.
*
* @return array Parsed metadata configuration.
*
* @throws Exception If something is wrong in the configuration.
*/
public static function parseSources($sourcesConfig)
{
assert(is_array($sourcesConfig));
$sources = array();
foreach ($sourcesConfig as $sourceConfig) {
if (!is_array($sourceConfig)) {
throw new Exception("Found an element in metadata source configuration which wasn't an array.");
}
$sources[] = self::getSource($sourceConfig);
}
return $sources;
}
/**
* This function creates a metadata source based on the given configuration.
* The type of source is based on the 'type' parameter in the configuration.
* The default type is 'flatfile'.
*
* @param array $sourceConfig Associative array with the configuration for this metadata source.
*
* @return mixed An instance of a metadata source with the given configuration.
*
* @throws Exception If the metadata source type is invalid.
*/
public static function getSource($sourceConfig)
{
assert(is_array($sourceConfig));
if (array_key_exists('type', $sourceConfig)) {
$type = $sourceConfig['type'];
} else {
$type = 'flatfile';
}
switch ($type) {
case 'flatfile':
return new SimpleSAML_Metadata_MetaDataStorageHandlerFlatFile($sourceConfig);
case 'xml':
return new SimpleSAML_Metadata_MetaDataStorageHandlerXML($sourceConfig);
case 'serialize':
return new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize($sourceConfig);
case 'mdx':
case 'mdq':
return new \SimpleSAML\Metadata\Sources\MDQ($sourceConfig);
case 'pdo':
return new SimpleSAML_Metadata_MetaDataStorageHandlerPdo($sourceConfig);
default:
// metadata store from module
try {
$className = SimpleSAML\Module::resolveClass(
$type,
'MetadataStore',
'SimpleSAML_Metadata_MetaDataStorageSource'
);
} catch (Exception $e) {
throw new SimpleSAML\Error\CriticalConfigurationError(
"Invalid 'type' for metadata source. Cannot find store '$type'.",
null
);
}
return new $className($sourceConfig);
}
}
/**
* This function attempts to generate an associative array with metadata for all entities in the
* given set. The key of the array is the entity id.
*
* A subclass should override this function if it is able to easily generate this list.
*
* @param string $set The set we want to list metadata for.
*
* @return array An associative array with all entities in the given set, or an empty array if we are
* unable to generate this list.
*/
public function getMetadataSet($set)
{
return array();
}
/**
* This function resolves an host/path combination to an entity id.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $hostPath The host/path combination we are looking up.
* @param string $set Which set of metadata we are looking it up in.
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string|null An entity id which matches the given host/path combination, or NULL if
* we are unable to locate one which matches.
*/
public function getEntityIdFromHostPath($hostPath, $set, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
if ($metadataSet === null) {
// this metadata source does not have this metadata set
return null;
}
foreach ($metadataSet as $index => $entry) {
if (!array_key_exists('host', $entry)) {
continue;
}
if ($hostPath === $entry['host']) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
// no entries matched, we should return null
return null;
}
/**
* This function will go through all the metadata, and check the DiscoHints->IPHint
* parameter, which defines a network space (ip range) for each remote entry.
* This function returns the entityID for any of the entities that have an
* IP range which the IP falls within.
*
* @param string $set Which set of metadata we are looking it up in.
* @param string $ip IP address
* @param string $type Do you want to return the metaindex or the entityID. [entityid|metaindex]
*
* @return string The entity id of a entity which have a CIDR hint where the provided
* IP address match.
*/
public function getPreferredEntityIdFromCIDRhint($set, $ip, $type = 'entityid')
{
$metadataSet = $this->getMetadataSet($set);
foreach ($metadataSet as $index => $entry) {
$cidrHints = array();
// support hint.cidr for idp discovery
if (array_key_exists('hint.cidr', $entry) && is_array($entry['hint.cidr'])) {
$cidrHints = $entry['hint.cidr'];
}
// support discohints in idp metadata for idp discovery
if (array_key_exists('DiscoHints', $entry)
&& array_key_exists('IPHint', $entry['DiscoHints'])
&& is_array($entry['DiscoHints']['IPHint'])) {
// merge with hints derived from discohints, but prioritize hint.cidr in case it is used
$cidrHints = array_merge($entry['DiscoHints']['IPHint'], $cidrHints);
}
if (empty($cidrHints)) {
continue;
}
foreach ($cidrHints as $hint_entry) {
if (SimpleSAML\Utils\Net::ipCIDRcheck($hint_entry, $ip)) {
if ($type === 'entityid') {
return $entry['entityid'];
} else {
return $index;
}
}
}
}
// no entries matched, we should return null
return null;
}
/*
*
*/
private function lookupIndexFromEntityId($entityId, $set)
{
assert(is_string($entityId));
assert(isset($set));
$metadataSet = $this->getMetadataSet($set);
// check for hostname
$currenthost = \SimpleSAML\Utils\HTTP::getSelfHost(); // sp.example.org
foreach ($metadataSet as $index => $entry) {
if ($index === $entityId) {
return $index;
}
if ($entry['entityid'] === $entityId) {
if ($entry['host'] === '__DEFAULT__' || $entry['host'] === $currenthost) {
return $index;
}
}
}
return null;
}
/**
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(isset($set));
$metadataSet = $this->getMetadataSet($set);
if (array_key_exists($index, $metadataSet)) {
return $metadataSet[$index];
}
$indexlookup = $this->lookupIndexFromEntityId($index, $set);
if (isset($indexlookup) && array_key_exists($indexlookup, $metadataSet)) {
return $metadataSet[$indexlookup];
}
return null;
}
}

View File

@@ -0,0 +1,777 @@
<?php
/**
* Class for generating SAML 2.0 metadata from SimpleSAMLphp metadata arrays.
*
* This class builds SAML 2.0 metadata for an entity by examining the metadata for the entity.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_SAMLBuilder
{
/**
* The EntityDescriptor we are building.
*
* @var \SAML2\XML\md\EntityDescriptor
*/
private $entityDescriptor;
/**
* The maximum time in seconds the metadata should be cached.
*
* @var int|null
*/
private $maxCache = null;
/**
* The maximum time in seconds since the current time that this metadata should be considered valid.
*
* @var int|null
*/
private $maxDuration = null;
/**
* Initialize the SAML builder.
*
* @param string $entityId The entity id of the entity.
* @param double|null $maxCache The maximum time in seconds the metadata should be cached. Defaults to null
* @param double|null $maxDuration The maximum time in seconds this metadata should be considered valid. Defaults
* to null.
*/
public function __construct($entityId, $maxCache = null, $maxDuration = null)
{
assert(is_string($entityId));
$this->maxCache = $maxCache;
$this->maxDuration = $maxDuration;
$this->entityDescriptor = new \SAML2\XML\md\EntityDescriptor();
$this->entityDescriptor->entityID = $entityId;
}
private function setExpiration($metadata)
{
if (array_key_exists('expire', $metadata)) {
if ($metadata['expire'] - time() < $this->maxDuration) {
$this->maxDuration = $metadata['expire'] - time();
}
}
if ($this->maxCache !== null) {
$this->entityDescriptor->cacheDuration = 'PT'.$this->maxCache.'S';
}
if ($this->maxDuration !== null) {
$this->entityDescriptor->validUntil = time() + $this->maxDuration;
}
}
/**
* Retrieve the EntityDescriptor element which is generated for this entity.
*
* @return DOMElement The EntityDescriptor element of this entity.
*/
public function getEntityDescriptor()
{
$xml = $this->entityDescriptor->toXML();
$xml->ownerDocument->appendChild($xml);
return $xml;
}
/**
* Retrieve the EntityDescriptor as text.
*
* This function serializes this EntityDescriptor, and returns it as text.
*
* @param bool $formatted Whether the returned EntityDescriptor should be formatted first.
*
* @return string The serialized EntityDescriptor.
*/
public function getEntityDescriptorText($formatted = true)
{
assert(is_bool($formatted));
$xml = $this->getEntityDescriptor();
if ($formatted) {
SimpleSAML\Utils\XML::formatDOMElement($xml);
}
return $xml->ownerDocument->saveXML();
}
/**
* Add a SecurityTokenServiceType for ADFS metadata.
*
* @param array $metadata The metadata with the information about the SecurityTokenServiceType.
*/
public function addSecurityTokenServiceType($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$defaultEndpoint = $metadata->getDefaultEndpoint('SingleSignOnService');
$e = new sspmod_adfs_SAML2_XML_fed_SecurityTokenServiceType();
$e->Location = $defaultEndpoint['Location'];
$this->addCertificate($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add extensions to the metadata.
*
* @param SimpleSAML_Configuration $metadata The metadata to get extensions from.
* @param \SAML2\XML\md\RoleDescriptor $e Reference to the element where the Extensions element should be included.
*/
private function addExtensions(SimpleSAML_Configuration $metadata, \SAML2\XML\md\RoleDescriptor $e)
{
if ($metadata->hasValue('tags')) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = 'tags';
foreach ($metadata->getArray('tags') as $tag) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($tag);
}
$e->Extensions[] = $a;
}
if ($metadata->hasValue('hint.cidr')) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = 'hint.cidr';
foreach ($metadata->getArray('hint.cidr') as $hint) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($hint);
}
$e->Extensions[] = $a;
}
if ($metadata->hasValue('scope')) {
foreach ($metadata->getArray('scope') as $scopetext) {
$s = new \SAML2\XML\shibmd\Scope();
$s->scope = $scopetext;
// Check whether $ ^ ( ) * | \ are in a scope -> assume regex.
if (1 === preg_match('/[\$\^\)\(\*\|\\\\]/', $scopetext)) {
$s->regexp = true;
} else {
$s->regexp = false;
}
$e->Extensions[] = $s;
}
}
if ($metadata->hasValue('EntityAttributes')) {
$ea = new \SAML2\XML\mdattr\EntityAttributes();
foreach ($metadata->getArray('EntityAttributes') as $attributeName => $attributeValues) {
$a = new \SAML2\XML\saml\Attribute();
$a->Name = $attributeName;
$a->NameFormat = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri';
// Attribute names that is not URI is prefixed as this: '{nameformat}name'
if (preg_match('/^\{(.*?)\}(.*)$/', $attributeName, $matches)) {
$a->Name = $matches[2];
$nameFormat = $matches[1];
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
$a->NameFormat = $nameFormat;
}
}
foreach ($attributeValues as $attributeValue) {
$a->AttributeValue[] = new \SAML2\XML\saml\AttributeValue($attributeValue);
}
$ea->children[] = $a;
}
$this->entityDescriptor->Extensions[] = $ea;
}
if ($metadata->hasValue('RegistrationInfo')) {
$ri = new \SAML2\XML\mdrpi\RegistrationInfo();
foreach ($metadata->getArray('RegistrationInfo') as $riName => $riValues) {
switch ($riName) {
case 'authority':
$ri->registrationAuthority = $riValues;
break;
case 'instant':
$ri->registrationInstant = \SAML2\Utils::xsDateTimeToTimestamp($riValues);
break;
case 'policies':
$ri->RegistrationPolicy = $riValues;
break;
}
}
$this->entityDescriptor->Extensions[] = $ri;
}
if ($metadata->hasValue('UIInfo')) {
$ui = new \SAML2\XML\mdui\UIInfo();
foreach ($metadata->getArray('UIInfo') as $uiName => $uiValues) {
switch ($uiName) {
case 'DisplayName':
$ui->DisplayName = $uiValues;
break;
case 'Description':
$ui->Description = $uiValues;
break;
case 'InformationURL':
$ui->InformationURL = $uiValues;
break;
case 'PrivacyStatementURL':
$ui->PrivacyStatementURL = $uiValues;
break;
case 'Keywords':
foreach ($uiValues as $lang => $keywords) {
$uiItem = new \SAML2\XML\mdui\Keywords();
$uiItem->lang = $lang;
$uiItem->Keywords = $keywords;
$ui->Keywords[] = $uiItem;
}
break;
case 'Logo':
foreach ($uiValues as $logo) {
$uiItem = new \SAML2\XML\mdui\Logo();
$uiItem->url = $logo['url'];
$uiItem->width = $logo['width'];
$uiItem->height = $logo['height'];
if (isset($logo['lang'])) {
$uiItem->lang = $logo['lang'];
}
$ui->Logo[] = $uiItem;
}
break;
}
}
$e->Extensions[] = $ui;
}
if ($metadata->hasValue('DiscoHints')) {
$dh = new \SAML2\XML\mdui\DiscoHints();
foreach ($metadata->getArray('DiscoHints') as $dhName => $dhValues) {
switch ($dhName) {
case 'IPHint':
$dh->IPHint = $dhValues;
break;
case 'DomainHint':
$dh->DomainHint = $dhValues;
break;
case 'GeolocationHint':
$dh->GeolocationHint = $dhValues;
break;
}
}
$e->Extensions[] = $dh;
}
}
/**
* Add an Organization element based on data passed as parameters
*
* @param array $orgName An array with the localized OrganizationName.
* @param array $orgDisplayName An array with the localized OrganizationDisplayName.
* @param array $orgURL An array with the localized OrganizationURL.
*/
public function addOrganization(array $orgName, array $orgDisplayName, array $orgURL)
{
$org = new \SAML2\XML\md\Organization();
$org->OrganizationName = $orgName;
$org->OrganizationDisplayName = $orgDisplayName;
$org->OrganizationURL = $orgURL;
$this->entityDescriptor->Organization = $org;
}
/**
* Add an Organization element based on metadata array.
*
* @param array $metadata The metadata we should extract the organization information from.
*/
public function addOrganizationInfo(array $metadata)
{
if (empty($metadata['OrganizationName']) ||
empty($metadata['OrganizationDisplayName']) ||
empty($metadata['OrganizationURL'])
) {
// empty or incomplete organization information
return;
}
$orgName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationName'], 'en');
$orgDisplayName = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationDisplayName'], 'en');
$orgURL = SimpleSAML\Utils\Arrays::arrayize($metadata['OrganizationURL'], 'en');
$this->addOrganization($orgName, $orgDisplayName, $orgURL);
}
/**
* Add a list of endpoints to metadata.
*
* @param array $endpoints The endpoints.
* @param bool $indexed Whether the endpoints should be indexed.
*
* @return array An array of endpoint objects, either \SAML2\XML\md\EndpointType or \SAML2\XML\md\IndexedEndpointType.
*/
private static function createEndpoints(array $endpoints, $indexed)
{
assert(is_bool($indexed));
$ret = array();
foreach ($endpoints as &$ep) {
if ($indexed) {
$t = new \SAML2\XML\md\IndexedEndpointType();
} else {
$t = new \SAML2\XML\md\EndpointType();
}
$t->Binding = $ep['Binding'];
$t->Location = $ep['Location'];
if (isset($ep['ResponseLocation'])) {
$t->ResponseLocation = $ep['ResponseLocation'];
}
if (isset($ep['hoksso:ProtocolBinding'])) {
$t->setAttributeNS(
\SAML2\Constants::NS_HOK,
'hoksso:ProtocolBinding',
\SAML2\Constants::BINDING_HTTP_REDIRECT
);
}
if ($indexed) {
if (!isset($ep['index'])) {
// Find the maximum index
$maxIndex = -1;
foreach ($endpoints as $ep) {
if (!isset($ep['index'])) {
continue;
}
if ($ep['index'] > $maxIndex) {
$maxIndex = $ep['index'];
}
}
$ep['index'] = $maxIndex + 1;
}
$t->index = $ep['index'];
}
$ret[] = $t;
}
return $ret;
}
/**
* Add an AttributeConsumingService element to the metadata.
*
* @param \SAML2\XML\md\SPSSODescriptor $spDesc The SPSSODescriptor element.
* @param SimpleSAML_Configuration $metadata The metadata.
*/
private function addAttributeConsumingService(
\SAML2\XML\md\SPSSODescriptor $spDesc,
SimpleSAML_Configuration $metadata
) {
$attributes = $metadata->getArray('attributes', array());
$name = $metadata->getLocalizedString('name', null);
if ($name === null || count($attributes) == 0) {
// we cannot add an AttributeConsumingService without name and attributes
return;
}
$attributesrequired = $metadata->getArray('attributes.required', array());
/*
* Add an AttributeConsumingService element with information as name and description and list
* of requested attributes
*/
$attributeconsumer = new \SAML2\XML\md\AttributeConsumingService();
$attributeconsumer->index = $metadata->getInteger('attributes.index', 0);
if ($metadata->hasValue('attributes.isDefault')) {
$attributeconsumer->isDefault = $metadata->getBoolean('attributes.isDefault', false);
}
$attributeconsumer->ServiceName = $name;
$attributeconsumer->ServiceDescription = $metadata->getLocalizedString('description', array());
$nameFormat = $metadata->getString('attributes.NameFormat', \SAML2\Constants::NAMEFORMAT_UNSPECIFIED);
foreach ($attributes as $friendlyName => $attribute) {
$t = new \SAML2\XML\md\RequestedAttribute();
$t->Name = $attribute;
if (!is_int($friendlyName)) {
$t->FriendlyName = $friendlyName;
}
if ($nameFormat !== \SAML2\Constants::NAMEFORMAT_UNSPECIFIED) {
$t->NameFormat = $nameFormat;
}
if (in_array($attribute, $attributesrequired, true)) {
$t->isRequired = true;
}
$attributeconsumer->RequestedAttribute[] = $t;
}
$spDesc->AttributeConsumingService[] = $attributeconsumer;
}
/**
* Add a specific type of metadata to an entity.
*
* @param string $set The metadata set this metadata comes from.
* @param array $metadata The metadata.
*/
public function addMetadata($set, $metadata)
{
assert(is_string($set));
assert(is_array($metadata));
$this->setExpiration($metadata);
switch ($set) {
case 'saml20-sp-remote':
$this->addMetadataSP20($metadata);
break;
case 'saml20-idp-remote':
$this->addMetadataIdP20($metadata);
break;
case 'shib13-sp-remote':
$this->addMetadataSP11($metadata);
break;
case 'shib13-idp-remote':
$this->addMetadataIdP11($metadata);
break;
case 'attributeauthority-remote':
$this->addAttributeAuthority($metadata);
break;
default:
SimpleSAML\Logger::warning('Unable to generate metadata for unknown type \''.$set.'\'.');
}
}
/**
* Add SAML 2.0 SP metadata.
*
* @param array $metadata The metadata.
* @param array $protocols The protocols supported. Defaults to \SAML2\Constants::NS_SAMLP.
*/
public function addMetadataSP20($metadata, $protocols = array(\SAML2\Constants::NS_SAMLP))
{
assert(is_array($metadata));
assert(is_array($protocols));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\SPSSODescriptor();
$e->protocolSupportEnumeration = $protocols;
if ($metadata->hasValue('saml20.sign.assertion')) {
$e->WantAssertionsSigned = $metadata->getBoolean('saml20.sign.assertion');
}
if ($metadata->hasValue('redirect.validate')) {
$e->AuthnRequestsSigned = $metadata->getBoolean('redirect.validate');
} elseif ($metadata->hasValue('validate.authnrequest')) {
$e->AuthnRequestsSigned = $metadata->getBoolean('validate.authnrequest');
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
$endpoints[] = array(
'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
'Location' => $acs,
);
}
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
foreach ($metadata->getArray('contacts', array()) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 2.0 identity provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataIdP20($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\IDPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:2.0:protocol';
if ($metadata->hasValue('sign.authnrequest')) {
$e->WantAuthnRequestsSigned = $metadata->getBoolean('sign.authnrequest');
} elseif ($metadata->hasValue('redirect.sign')) {
$e->WantAuthnRequestsSigned = $metadata->getBoolean('redirect.sign');
}
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
if ($metadata->hasValue('ArtifactResolutionService')) {
$e->ArtifactResolutionService = self::createEndpoints(
$metadata->getEndpoints('ArtifactResolutionService'),
true
);
}
$e->SingleLogoutService = self::createEndpoints($metadata->getEndpoints('SingleLogoutService'), false);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
$this->entityDescriptor->RoleDescriptor[] = $e;
foreach ($metadata->getArray('contacts', array()) as $contact) {
if (array_key_exists('contactType', $contact) && array_key_exists('emailAddress', $contact)) {
$this->addContact($contact['contactType'], \SimpleSAML\Utils\Config\Metadata::getContact($contact));
}
}
}
/**
* Add metadata of a SAML 1.1 service provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataSP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\SPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
$this->addCertificate($e, $metadata);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$endpoints = $metadata->getEndpoints('AssertionConsumerService');
foreach ($metadata->getArrayizeString('AssertionConsumerService.artifact', array()) as $acs) {
$endpoints[] = array(
'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:artifact-01',
'Location' => $acs,
);
}
$e->AssertionConsumerService = self::createEndpoints($endpoints, true);
$this->addAttributeConsumingService($e, $metadata);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add metadata of a SAML 1.1 identity provider.
*
* @param array $metadata The metadata.
*/
public function addMetadataIdP11($metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\IDPSSODescriptor();
$e->protocolSupportEnumeration[] = 'urn:oasis:names:tc:SAML:1.1:protocol';
$e->protocolSupportEnumeration[] = 'urn:mace:shibboleth:1.0';
$this->addCertificate($e, $metadata);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$e->SingleSignOnService = self::createEndpoints($metadata->getEndpoints('SingleSignOnService'), false);
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add metadata of a SAML attribute authority.
*
* @param array $metadata The AttributeAuthorityDescriptor, in the format returned by
* SimpleSAML_Metadata_SAMLParser.
*/
public function addAttributeAuthority(array $metadata)
{
assert(is_array($metadata));
assert(isset($metadata['entityid']));
assert(isset($metadata['metadata-set']));
$metadata = SimpleSAML_Configuration::loadFromArray($metadata, $metadata['entityid']);
$e = new \SAML2\XML\md\AttributeAuthorityDescriptor();
$e->protocolSupportEnumeration = $metadata->getArray('protocols', array(\SAML2\Constants::NS_SAMLP));
$this->addExtensions($metadata, $e);
$this->addCertificate($e, $metadata);
$e->AttributeService = self::createEndpoints($metadata->getEndpoints('AttributeService'), false);
$e->AssertionIDRequestService = self::createEndpoints(
$metadata->getEndpoints('AssertionIDRequestService'),
false
);
$e->NameIDFormat = $metadata->getArrayizeString('NameIDFormat', array());
$this->entityDescriptor->RoleDescriptor[] = $e;
}
/**
* Add contact information.
*
* Accepts a contact type, and a contact array that must be previously sanitized.
*
* WARNING: This function will change its signature and no longer parse a 'name' element.
*
* @param string $type The type of contact. Deprecated.
* @param array $details The details about the contact.
*
* @todo Change the signature to remove $type.
* @todo Remove the capability to pass a name and parse it inside the method.
*/
public function addContact($type, $details)
{
assert(is_string($type));
assert(is_array($details));
assert(in_array($type, array('technical', 'support', 'administrative', 'billing', 'other'), true));
// TODO: remove this check as soon as getContact() is called always before calling this function
$details = \SimpleSAML\Utils\Config\Metadata::getContact($details);
$e = new \SAML2\XML\md\ContactPerson();
$e->contactType = $type;
if (!empty($details['attributes'])) {
$e->ContactPersonAttributes = $details['attributes'];
}
if (isset($details['company'])) {
$e->Company = $details['company'];
}
if (isset($details['givenName'])) {
$e->GivenName = $details['givenName'];
}
if (isset($details['surName'])) {
$e->SurName = $details['surName'];
}
if (isset($details['emailAddress'])) {
$eas = $details['emailAddress'];
if (!is_array($eas)) {
$eas = array($eas);
}
foreach ($eas as $ea) {
$e->EmailAddress[] = $ea;
}
}
if (isset($details['telephoneNumber'])) {
$tlfNrs = $details['telephoneNumber'];
if (!is_array($tlfNrs)) {
$tlfNrs = array($tlfNrs);
}
foreach ($tlfNrs as $tlfNr) {
$e->TelephoneNumber[] = $tlfNr;
}
}
$this->entityDescriptor->ContactPerson[] = $e;
}
/**
* Add a KeyDescriptor with an X509 certificate.
*
* @param \SAML2\XML\md\RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param string $use The value of the 'use' attribute.
* @param string $x509data The certificate data.
*/
private function addX509KeyDescriptor(\SAML2\XML\md\RoleDescriptor $rd, $use, $x509data)
{
assert(in_array($use, array('encryption', 'signing'), true));
assert(is_string($x509data));
$keyDescriptor = \SAML2\Utils::createKeyDescriptor($x509data);
$keyDescriptor->use = $use;
$rd->KeyDescriptor[] = $keyDescriptor;
}
/**
* Add a certificate.
*
* Helper function for adding a certificate to the metadata.
*
* @param \SAML2\XML\md\RoleDescriptor $rd The RoleDescriptor the certificate should be added to.
* @param SimpleSAML_Configuration $metadata The metadata of the entity.
*/
private function addCertificate(\SAML2\XML\md\RoleDescriptor $rd, SimpleSAML_Configuration $metadata)
{
$keys = $metadata->getPublicKeys();
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if (!isset($key['signing']) || $key['signing'] === true) {
$this->addX509KeyDescriptor($rd, 'signing', $key['X509Certificate']);
}
if (!isset($key['encryption']) || $key['encryption'] === true) {
$this->addX509KeyDescriptor($rd, 'encryption', $key['X509Certificate']);
}
}
if ($metadata->hasValue('https.certData')) {
$this->addX509KeyDescriptor($rd, 'signing', $metadata->getString('https.certData'));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
<?php
/**
* This class implements a helper function for signing of metadata.
*
* @author Olav Morken, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_Metadata_Signer
{
/**
* This functions finds what key & certificate files should be used to sign the metadata
* for the given entity.
*
* @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return array An associative array with the keys 'privatekey', 'certificate', and optionally 'privatekey_pass'.
* @throws Exception If the key and certificate used to sign is unknown.
*/
private static function findKeyCert($config, $entityMetadata, $type)
{
// first we look for metadata.privatekey and metadata.certificate in the metadata
if (array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
if (!array_key_exists('metadata.sign.privatekey', $entityMetadata)
|| !array_key_exists('metadata.sign.certificate', $entityMetadata)
) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the'.
' "metadata.sign.certificate" configuration option in the metadata for'.
' the '.$type.' "'.$entityMetadata['entityid'].'". If one of'.
' these options is specified, then the other must also be specified.'
);
}
$ret = array(
'privatekey' => $entityMetadata['metadata.sign.privatekey'],
'certificate' => $entityMetadata['metadata.sign.certificate']
);
if (array_key_exists('metadata.sign.privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['metadata.sign.privatekey_pass'];
}
return $ret;
}
// then we look for default values in the global configuration
$privatekey = $config->getString('metadata.sign.privatekey', null);
$certificate = $config->getString('metadata.sign.certificate', null);
if ($privatekey !== null || $certificate !== null) {
if ($privatekey === null || $certificate === null) {
throw new Exception(
'Missing either the "metadata.sign.privatekey" or the'.
' "metadata.sign.certificate" configuration option in the global'.
' configuration. If one of these options is specified, then the other'.
' must also be specified.'
);
}
$ret = array('privatekey' => $privatekey, 'certificate' => $certificate);
$privatekey_pass = $config->getString('metadata.sign.privatekey_pass', null);
if ($privatekey_pass !== null) {
$ret['privatekey_pass'] = $privatekey_pass;
}
return $ret;
}
// as a last resort we attempt to use the privatekey and certificate option from the metadata
if (array_key_exists('privatekey', $entityMetadata)
|| array_key_exists('certificate', $entityMetadata)
) {
if (!array_key_exists('privatekey', $entityMetadata)
|| !array_key_exists('certificate', $entityMetadata)
) {
throw new Exception(
'Both the "privatekey" and the "certificate" option must'.
' be set in the metadata for the '.$type.' "'.
$entityMetadata['entityid'].'" before it is possible to sign metadata'.
' from this entity.'
);
}
$ret = array(
'privatekey' => $entityMetadata['privatekey'],
'certificate' => $entityMetadata['certificate']
);
if (array_key_exists('privatekey_pass', $entityMetadata)) {
$ret['privatekey_pass'] = $entityMetadata['privatekey_pass'];
}
return $ret;
}
throw new Exception(
'Could not find what key & certificate should be used to sign the metadata'.
' for the '.$type.' "'.$entityMetadata['entityid'].'".'
);
}
/**
* Determine whether metadata signing is enabled for the given metadata.
*
* @param SimpleSAML_Configuration $config Our SimpleSAML_Configuration instance.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or
* 'Shib 1.3 SP'.
*
* @return boolean True if metadata signing is enabled, false otherwise.
* @throws Exception If the value of the 'metadata.sign.enable' option is not a boolean.
*/
private static function isMetadataSigningEnabled($config, $entityMetadata, $type)
{
// first check the metadata for the entity
if (array_key_exists('metadata.sign.enable', $entityMetadata)) {
if (!is_bool($entityMetadata['metadata.sign.enable'])) {
throw new Exception(
'Invalid value for the "metadata.sign.enable" configuration option for'.
' the '.$type.' "'.$entityMetadata['entityid'].'". This option'.
' should be a boolean.'
);
}
return $entityMetadata['metadata.sign.enable'];
}
$enabled = $config->getBoolean('metadata.sign.enable', false);
return $enabled;
}
/**
* Determine the signature and digest algorithms to use when signing metadata.
*
* This method will look for the 'metadata.sign.algorithm' key in the $entityMetadata array, or look for such
* a configuration option in the $config object.
*
* @param SimpleSAML_Configuration $config The global configuration.
* @param array $entityMetadata An array containing the metadata related to this entity.
* @param string $type A string describing the type of entity. E.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return array An array with two keys, 'algorithm' and 'digest', corresponding to the signature and digest
* algorithms to use, respectively.
*
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
private static function getMetadataSigningAlgorithm($config, $entityMetadata, $type)
{
// configure the algorithm to use
if (array_key_exists('metadata.sign.algorithm', $entityMetadata)) {
if (!is_string($entityMetadata['metadata.sign.algorithm'])) {
throw new \SimpleSAML\Error\CriticalConfigurationError(
"Invalid value for the 'metadata.sign.algorithm' configuration option for the ".$type.
"'".$entityMetadata['entityid']."'. This option has restricted values"
);
}
$alg = $entityMetadata['metadata.sign.algorithm'];
} else {
$alg = $config->getString('metadata.sign.algorithm', XMLSecurityKey::RSA_SHA256);
}
$supported_algs = array(
XMLSecurityKey::RSA_SHA1,
XMLSecurityKey::RSA_SHA256,
XMLSecurityKey::RSA_SHA384,
XMLSecurityKey::RSA_SHA512,
);
if (!in_array($alg, $supported_algs, true)) {
throw new \SimpleSAML\Error\CriticalConfigurationError("Unknown signature algorithm '$alg'");
}
switch ($alg) {
case XMLSecurityKey::RSA_SHA256:
$digest = XMLSecurityDSig::SHA256;
break;
case XMLSecurityKey::RSA_SHA384:
$digest = XMLSecurityDSig::SHA384;
break;
case XMLSecurityKey::RSA_SHA512:
$digest = XMLSecurityDSig::SHA512;
break;
default:
$digest = XMLSecurityDSig::SHA1;
}
return array(
'algorithm' => $alg,
'digest' => $digest,
);
}
/**
* Signs the given metadata if metadata signing is enabled.
*
* @param string $metadataString A string with the metadata.
* @param array $entityMetadata The metadata of the entity.
* @param string $type A string which describes the type entity this is, e.g. 'SAML 2 IdP' or 'Shib 1.3 SP'.
*
* @return string The $metadataString with the signature embedded.
* @throws Exception If the certificate or private key cannot be loaded, or the metadata doesn't parse properly.
*/
public static function sign($metadataString, $entityMetadata, $type)
{
$config = SimpleSAML_Configuration::getInstance();
// check if metadata signing is enabled
if (!self::isMetadataSigningEnabled($config, $entityMetadata, $type)) {
return $metadataString;
}
// find the key & certificate which should be used to sign the metadata
$keyCertFiles = self::findKeyCert($config, $entityMetadata, $type);
$keyFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['privatekey']);
if (!file_exists($keyFile)) {
throw new Exception('Could not find private key file ['.$keyFile.'], which is needed to sign the metadata');
}
$keyData = file_get_contents($keyFile);
$certFile = \SimpleSAML\Utils\Config::getCertPath($keyCertFiles['certificate']);
if (!file_exists($certFile)) {
throw new Exception(
'Could not find certificate file ['.$certFile.'], which is needed to sign the metadata'
);
}
$certData = file_get_contents($certFile);
// convert the metadata to a DOM tree
try {
$xml = \SAML2\DOMDocumentFactory::fromString($metadataString);
} catch (Exception $e) {
throw new Exception('Error parsing self-generated metadata.');
}
$signature_cf = self::getMetadataSigningAlgorithm($config, $entityMetadata, $type);
// load the private key
$objKey = new XMLSecurityKey($signature_cf['algorithm'], array('type' => 'private'));
if (array_key_exists('privatekey_pass', $keyCertFiles)) {
$objKey->passphrase = $keyCertFiles['privatekey_pass'];
}
$objKey->loadKey($keyData, false);
// get the EntityDescriptor node we should sign
$rootNode = $xml->firstChild;
// sign the metadata with our private key
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objXMLSecDSig->addReferenceList(
array($rootNode),
$signature_cf['digest'],
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N),
array('id_name' => 'ID')
);
$objXMLSecDSig->sign($objKey);
// add the certificate to the signature
$objXMLSecDSig->add509Cert($certData, true);
// add the signature to the metadata
$objXMLSecDSig->insertSignature($rootNode, $rootNode->firstChild);
// return the DOM tree as a string
return $xml->saveXML();
}
}

View File

@@ -0,0 +1,337 @@
<?php
namespace SimpleSAML\Metadata\Sources;
use SimpleSAML\Logger;
use SimpleSAML\Utils\HTTP;
/**
* This class implements SAML Metadata Query Protocol
*
* @author Andreas Åkre Solberg, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Tamas Frank, NIIFI
* @package SimpleSAMLphp
*/
class MDQ extends \SimpleSAML_Metadata_MetaDataStorageSource
{
/**
* The URL of MDQ server (url:port)
*
* @var string
*/
private $server;
/**
* The fingerprint of the certificate used to sign the metadata. You don't need this option if you don't want to
* validate the signature on the metadata.
*
* @var string|null
*/
private $validateFingerprint;
/**
* The cache directory, or null if no cache directory is configured.
*
* @var string|null
*/
private $cacheDir;
/**
* The maximum cache length, in seconds.
*
* @var integer
*/
private $cacheLength;
/**
* This function initializes the dynamic XML metadata source.
*
* Options:
* - 'server': URL of the MDQ server (url:port). Mandatory.
* - 'validateFingerprint': The fingerprint of the certificate used to sign the metadata.
* You don't need this option if you don't want to validate the signature on the metadata.
* Optional.
* - 'cachedir': Directory where metadata can be cached. Optional.
* - 'cachelength': Maximum time metadata cah be cached, in seconds. Default to 24
* hours (86400 seconds).
*
* @param array $config The configuration for this instance of the XML metadata source.
*
* @throws \Exception If no server option can be found in the configuration.
*/
protected function __construct($config)
{
assert(is_array($config));
if (!array_key_exists('server', $config)) {
throw new \Exception(__CLASS__.": the 'server' configuration option is not set.");
} else {
$this->server = $config['server'];
}
if (array_key_exists('validateFingerprint', $config)) {
$this->validateFingerprint = $config['validateFingerprint'];
} else {
$this->validateFingerprint = null;
}
if (array_key_exists('cachedir', $config)) {
$globalConfig = \SimpleSAML_Configuration::getInstance();
$this->cacheDir = $globalConfig->resolvePath($config['cachedir']);
} else {
$this->cacheDir = null;
}
if (array_key_exists('cachelength', $config)) {
$this->cacheLength = $config['cachelength'];
} else {
$this->cacheLength = 86400;
}
}
/**
* This function is not implemented.
*
* @param string $set The set we want to list metadata for.
*
* @return array An empty array.
*/
public function getMetadataSet($set)
{
// we don't have this metadata set
return array();
}
/**
* Find the cache file name for an entity,
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return string The full path to the cache file.
*/
private function getCacheFilename($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
$cachekey = sha1($entityId);
return $this->cacheDir.'/'.$set.'-'.$cachekey.'.cached.xml';
}
/**
* Load a entity from the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
*
* @return array|NULL The associative array with the metadata for this entity, or NULL
* if the entity could not be found.
* @throws \Exception If an error occurs while loading metadata from cache.
*/
private function getFromCache($set, $entityId)
{
assert(is_string($set));
assert(is_string($entityId));
if (empty($this->cacheDir)) {
return null;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!file_exists($cachefilename)) {
return null;
}
if (!is_readable($cachefilename)) {
throw new \Exception(__CLASS__.': could not read cache file for entity ['.$cachefilename.']');
}
Logger::debug(__CLASS__.': reading cache ['.$entityId.'] => ['.$cachefilename.']');
/* Ensure that this metadata isn't older that the cachelength option allows. This
* must be verified based on the file, since this option may be changed after the
* file is written.
*/
$stat = stat($cachefilename);
if ($stat['mtime'] + $this->cacheLength <= time()) {
Logger::debug(__CLASS__.': cache file older that the cachelength option allows.');
return null;
}
$rawData = file_get_contents($cachefilename);
if (empty($rawData)) {
$error = error_get_last();
throw new \Exception(
__CLASS__.': error reading metadata from cache file "'.$cachefilename.'": '.$error['message']
);
}
$data = unserialize($rawData);
if ($data === false) {
throw new \Exception(__CLASS__.': error unserializing cached data from file "'.$cachefilename.'".');
}
if (!is_array($data)) {
throw new \Exception(__CLASS__.': Cached metadata from "'.$cachefilename.'" wasn\'t an array.');
}
return $data;
}
/**
* Save a entity to the cache.
*
* @param string $set The metadata set this entity belongs to.
* @param string $entityId The entity id of this entity.
* @param array $data The associative array with the metadata for this entity.
*
* @throws \Exception If metadata cannot be written to cache.
*/
private function writeToCache($set, $entityId, $data)
{
assert(is_string($set));
assert(is_string($entityId));
assert(is_array($data));
if (empty($this->cacheDir)) {
return;
}
$cachefilename = $this->getCacheFilename($set, $entityId);
if (!is_writable(dirname($cachefilename))) {
throw new \Exception(__CLASS__.': could not write cache file for entity ['.$cachefilename.']');
}
Logger::debug(__CLASS__.': Writing cache ['.$entityId.'] => ['.$cachefilename.']');
file_put_contents($cachefilename, serialize($data));
}
/**
* Retrieve metadata for the correct set from a SAML2Parser.
*
* @param \SimpleSAML_Metadata_SAMLParser $entity A SAML2Parser representing an entity.
* @param string $set The metadata set we are looking for.
*
* @return array|NULL The associative array with the metadata, or NULL if no metadata for
* the given set was found.
*/
private static function getParsedSet(\SimpleSAML_Metadata_SAMLParser $entity, $set)
{
assert(is_string($set));
switch ($set) {
case 'saml20-idp-remote':
return $entity->getMetadata20IdP();
case 'saml20-sp-remote':
return $entity->getMetadata20SP();
case 'shib13-idp-remote':
return $entity->getMetadata1xIdP();
case 'shib13-sp-remote':
return $entity->getMetadata1xSP();
case 'attributeauthority-remote':
$ret = $entity->getAttributeAuthorities();
return $ret[0];
default:
Logger::warning(__CLASS__.': unknown metadata set: \''.$set.'\'.');
}
return null;
}
/**
* Overriding this function from the superclass SimpleSAML_Metadata_MetaDataStorageSource.
*
* This function retrieves metadata for the given entity id in the given set of metadata.
* It will return NULL if it is unable to locate the metadata.
*
* This class implements this function using the getMetadataSet-function. A subclass should
* override this function if it doesn't implement the getMetadataSet function, or if the
* implementation of getMetadataSet is slow.
*
* @param string $index The entityId or metaindex we are looking up.
* @param string $set The set we are looking for metadata in.
*
* @return array An associative array with metadata for the given entity, or NULL if we are unable to
* locate the entity.
* @throws \Exception If an error occurs while validating the signature or the metadata is in an
* incorrect set.
*/
public function getMetaData($index, $set)
{
assert(is_string($index));
assert(is_string($set));
Logger::info(__CLASS__.': loading metadata entity ['.$index.'] from ['.$set.']');
// read from cache if possible
try {
$data = $this->getFromCache($set, $index);
} catch (\Exception $e) {
Logger::error($e->getMessage());
// proceed with fetching metadata even if the cache is broken
$data = null;
}
if ($data !== null && array_key_exists('expires', $data) && $data['expires'] < time()) {
// metadata has expired
$data = null;
}
if (isset($data)) {
// metadata found in cache and not expired
Logger::debug(__CLASS__.': using cached metadata for: '.$index.'.');
return $data;
}
// look at Metadata Query Protocol: https://github.com/iay/md-query/blob/master/draft-young-md-query.txt
$mdq_url = $this->server.'/entities/'.urlencode($index);
Logger::debug(__CLASS__.': downloading metadata for "'.$index.'" from ['.$mdq_url.']');
try {
$xmldata = HTTP::fetch($mdq_url);
} catch (\Exception $e) {
// Avoid propagating the exception, make sure we can handle the error later
$xmldata = false;
}
if (empty($xmldata)) {
$error = error_get_last();
Logger::info('Unable to fetch metadata for "'.$index.'" from '.$mdq_url.': '.
(is_array($error) ? $error['message'] : 'no error available'));
return null;
}
/** @var string $xmldata */
$entity = \SimpleSAML_Metadata_SAMLParser::parseString($xmldata);
Logger::debug(__CLASS__.': completed parsing of ['.$mdq_url.']');
if ($this->validateFingerprint !== null) {
if (!$entity->validateFingerprint($this->validateFingerprint)) {
throw new \Exception(__CLASS__.': error, could not verify signature for entity: '.$index.'".');
}
}
$data = self::getParsedSet($entity, $set);
if ($data === null) {
throw new \Exception(__CLASS__.': no metadata for set "'.$set.'" available from "'.$index.'".');
}
try {
$this->writeToCache($set, $index, $data);
} catch (\Exception $e) {
// Proceed without writing to cache
Logger::error('Error writing MDQ result to cache: '.$e->getMessage());
}
return $data;
}
}

311
lib/SimpleSAML/Module.php Executable file
View File

@@ -0,0 +1,311 @@
<?php
namespace SimpleSAML;
/**
* Helper class for accessing information about modules.
*
* @author Olav Morken <olav.morken@uninett.no>, UNINETT AS.
* @author Boy Baukema, SURFnet.
* @author Jaime Perez <jaime.perez@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class Module
{
/**
* A list containing the modules currently installed.
*
* @var array
*/
public static $modules = array();
/**
* A cache containing specific information for modules, like whether they are enabled or not, or their hooks.
*
* @var array
*/
public static $module_info = array();
/**
* Retrieve the base directory for a module.
*
* The returned path name will be an absolute path.
*
* @param string $module Name of the module
*
* @return string The base directory of a module.
*/
public static function getModuleDir($module)
{
$baseDir = dirname(dirname(dirname(__FILE__))).'/modules';
$moduleDir = $baseDir.'/'.$module;
return $moduleDir;
}
/**
* Determine whether a module is enabled.
*
* Will return false if the given module doesn't exist.
*
* @param string $module Name of the module
*
* @return bool True if the given module is enabled, false otherwise.
*
* @throws \Exception If module.enable is set and is not boolean.
*/
public static function isModuleEnabled($module)
{
$config = \SimpleSAML_Configuration::getOptionalConfig();
return self::isModuleEnabledWithConf($module, $config->getArray('module.enable', array()));
}
private static function isModuleEnabledWithConf($module, $mod_config)
{
if (isset(self::$module_info[$module]['enabled'])) {
return self::$module_info[$module]['enabled'];
}
if (!empty(self::$modules) && !in_array($module, self::$modules, true)) {
return false;
}
$moduleDir = self::getModuleDir($module);
if (!is_dir($moduleDir)) {
self::$module_info[$module]['enabled'] = false;
return false;
}
if (isset($mod_config[$module])) {
if (is_bool($mod_config[$module])) {
self::$module_info[$module]['enabled'] = $mod_config[$module];
return $mod_config[$module];
}
throw new \Exception("Invalid module.enable value for the '$module' module.");
}
if (assert_options(ASSERT_ACTIVE) &&
!file_exists($moduleDir.'/default-enable') &&
!file_exists($moduleDir.'/default-disable')
) {
\SimpleSAML\Logger::error("Missing default-enable or default-disable file for the module $module");
}
if (file_exists($moduleDir.'/enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
if (!file_exists($moduleDir.'/disable') && file_exists($moduleDir.'/default-enable')) {
self::$module_info[$module]['enabled'] = true;
return true;
}
self::$module_info[$module]['enabled'] = false;
return false;
}
/**
* Get available modules.
*
* @return array One string for each module.
*
* @throws \Exception If we cannot open the module's directory.
*/
public static function getModules()
{
if (!empty(self::$modules)) {
return self::$modules;
}
$path = self::getModuleDir('.');
$dh = scandir($path);
if ($dh === false) {
throw new \Exception('Unable to open module directory "'.$path.'".');
}
foreach ($dh as $f) {
if ($f[0] === '.') {
continue;
}
if (!is_dir($path.'/'.$f)) {
continue;
}
self::$modules[] = $f;
}
return self::$modules;
}
/**
* Resolve module class.
*
* This function takes a string on the form "<module>:<class>" and converts it to a class
* name. It can also check that the given class is a subclass of a specific class. The
* resolved classname will be "sspmod_<module>_<$type>_<class>.
*
* It is also possible to specify a full classname instead of <module>:<class>.
*
* An exception will be thrown if the class can't be resolved.
*
* @param string $id The string we should resolve.
* @param string $type The type of the class.
* @param string|null $subclass The class should be a subclass of this class. Optional.
*
* @return string The classname.
*
* @throws \Exception If the class cannot be resolved.
*/
public static function resolveClass($id, $type, $subclass = null)
{
assert(is_string($id));
assert(is_string($type));
assert(is_string($subclass) || $subclass === null);
$tmp = explode(':', $id, 2);
if (count($tmp) === 1) { // no module involved
$className = $tmp[0];
if (!class_exists($className)) {
throw new \Exception("Could not resolve '$id': no class named '$className'.");
}
} else { // should be a module
// make sure empty types are handled correctly
$type = (empty($type)) ? '_' : '_'.$type.'_';
// check for the old-style class names
$className = 'sspmod_'.$tmp[0].$type.$tmp[1];
if (!class_exists($className)) {
// check for the new-style class names, using namespaces
$type = str_replace('_', '\\', $type);
$newClassName = 'SimpleSAML\Module\\'.$tmp[0].$type.$tmp[1];
if (!class_exists($newClassName)) {
throw new \Exception("Could not resolve '$id': no class named '$className' or '$newClassName'.");
}
$className = $newClassName;
}
}
if ($subclass !== null && !is_subclass_of($className, $subclass)) {
throw new \Exception(
'Could not resolve \''.$id.'\': The class \''.$className.'\' isn\'t a subclass of \''.$subclass.'\'.'
);
}
return $className;
}
/**
* Get absolute URL to a specified module resource.
*
* This function creates an absolute URL to a resource stored under ".../modules/<module>/www/".
*
* @param string $resource Resource path, on the form "<module name>/<resource>"
* @param array $parameters Extra parameters which should be added to the URL. Optional.
*
* @return string The absolute URL to the given resource.
*/
public static function getModuleURL($resource, array $parameters = array())
{
assert(is_string($resource));
assert($resource[0] !== '/');
$url = Utils\HTTP::getBaseURL().'module.php/'.$resource;
if (!empty($parameters)) {
$url = Utils\HTTP::addURLParameters($url, $parameters);
}
return $url;
}
/**
* Get the available hooks for a given module.
*
* @param string $module The module where we should look for hooks.
*
* @return array An array with the hooks available for this module. Each element is an array with two keys: 'file'
* points to the file that contains the hook, and 'func' contains the name of the function implementing that hook.
* When there are no hooks defined, an empty array is returned.
*/
public static function getModuleHooks($module)
{
if (isset(self::$modules[$module]['hooks'])) {
return self::$modules[$module]['hooks'];
}
$hook_dir = self::getModuleDir($module).'/hooks';
if (!is_dir($hook_dir)) {
return array();
}
$hooks = array();
$files = scandir($hook_dir);
foreach ($files as $file) {
if ($file[0] === '.') {
continue;
}
if (!preg_match('/hook_(\w+)\.php/', $file, $matches)) {
continue;
}
$hook_name = $matches[1];
$hook_func = $module.'_hook_'.$hook_name;
$hooks[$hook_name] = array('file' => $hook_dir.'/'.$file, 'func' => $hook_func);
}
return $hooks;
}
/**
* Call a hook in all enabled modules.
*
* This function iterates over all enabled modules and calls a hook in each module.
*
* @param string $hook The name of the hook.
* @param mixed &$data The data which should be passed to each hook. Will be passed as a reference.
*
* @throws \SimpleSAML_Error_Exception If an invalid hook is found in a module.
*/
public static function callHooks($hook, &$data = null)
{
assert(is_string($hook));
$modules = self::getModules();
$config = \SimpleSAML_Configuration::getOptionalConfig()->getArray('module.enable', array());
sort($modules);
foreach ($modules as $module) {
if (!self::isModuleEnabledWithConf($module, $config)) {
continue;
}
if (!isset(self::$module_info[$module]['hooks'])) {
self::$module_info[$module]['hooks'] = self::getModuleHooks($module);
}
if (!isset(self::$module_info[$module]['hooks'][$hook])) {
continue;
}
require_once(self::$module_info[$module]['hooks'][$hook]['file']);
if (!is_callable(self::$module_info[$module]['hooks'][$hook]['func'])) {
throw new \SimpleSAML_Error_Exception('Invalid hook \''.$hook.'\' for module \''.$module.'\'.');
}
$fn = self::$module_info[$module]['hooks'][$hook]['func'];
$fn($data);
}
}
}

1171
lib/SimpleSAML/Session.php Executable file

File diff suppressed because it is too large Load Diff

160
lib/SimpleSAML/SessionHandler.php Executable file
View File

@@ -0,0 +1,160 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the
* root of the distribution for licence information.
*
* This file defines a base class for session handling.
* Instantiation of session handler objects should be done through
* the class method getSessionHandler().
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
abstract class SessionHandler
{
/**
* This static variable contains a reference to the current
* instance of the session handler. This variable will be NULL if
* we haven't instantiated a session handler yet.
*
* @var \SimpleSAML\SessionHandler
*/
protected static $sessionHandler = null;
/**
* This function retrieves the current instance of the session handler.
* The session handler will be instantiated if this is the first call
* to this function.
*
* @return \SimpleSAML\SessionHandler The current session handler.
*/
public static function getSessionHandler()
{
if (self::$sessionHandler === null) {
self::createSessionHandler();
}
return self::$sessionHandler;
}
/**
* This constructor is included in case it is needed in the
* future. Including it now allows us to write parent::__construct() in
* the subclasses of this class.
*/
protected function __construct()
{
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
abstract public function newSessionId();
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
abstract public function getCookieSessionId();
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
abstract public function getSessionCookieName();
/**
* Save the session.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
abstract public function saveSession(\SimpleSAML_Session $session);
/**
* Load the session.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*/
abstract public function loadSession($sessionId = null);
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return bool True if it was set, false if not.
*/
abstract public function hasSessionCookie();
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
abstract public function setCookie($sessionName, $sessionID, array $cookieParams = null);
/**
* Initialize the session handler.
*
* This function creates an instance of the session handler which is
* selected in the 'session.handler' configuration directive. If no
* session handler is selected, then we will fall back to the default
* PHP session handler.
*/
private static function createSessionHandler()
{
$store = \SimpleSAML\Store::getInstance();
if ($store === false) {
self::$sessionHandler = new SessionHandlerPHP();
} else {
/** @var \SimpleSAML\Store $store At this point, $store can only be an object */
self::$sessionHandler = new SessionHandlerStore($store);
}
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* @return array An array with the cookie parameters.
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
*/
public function getCookieParams()
{
$config = \SimpleSAML_Configuration::getInstance();
return array(
'lifetime' => $config->getInteger('session.cookie.lifetime', 0),
'path' => $config->getString('session.cookie.path', '/'),
'domain' => $config->getString('session.cookie.domain', null),
'secure' => $config->getBoolean('session.cookie.secure', false),
'httponly' => true,
);
}
}

View File

@@ -0,0 +1,173 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a base class for session handlers that need to store the session id in a cookie. It takes care of
* storing and retrieving the session id.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
* @abstract
*/
namespace SimpleSAML;
use SimpleSAML\Utils\HTTP;
abstract class SessionHandlerCookie extends SessionHandler
{
/**
* This variable contains the current session id.
*
* @var string|null
*/
private $session_id = null;
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* This constructor initializes the session id based on what we receive in a cookie. We create a new session id and
* set a cookie with this id if we don't have a session id.
*/
protected function __construct()
{
// call the constructor in the base class in case it should become necessary in the future
parent::__construct();
$config = \SimpleSAML_Configuration::getInstance();
$this->cookie_name = $config->getString('session.cookie.name', 'SimpleSAMLSessionID');
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
$this->session_id = self::createSessionID();
\SimpleSAML_Session::createSession($this->session_id);
return $this->session_id;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*/
public function getCookieSessionId()
{
if ($this->session_id === null) {
if (self::hasSessionCookie()) {
// attempt to retrieve the session id from the cookie
$this->session_id = $_COOKIE[$this->cookie_name];
}
// check if we have a valid session id
if (!self::isValidSessionID($this->session_id)) {
// invalid, disregard this session
return null;
}
}
return $this->session_id;
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* This static function creates a session id. A session id consists of 32 random hexadecimal characters.
*
* @return string A random session id.
*/
private static function createSessionID()
{
return bin2hex(openssl_random_pseudo_bytes(16));
}
/**
* This static function validates a session id. A session id is valid if it only consists of characters which are
* allowed in a session id and it is the correct length.
*
* @param string $session_id The session ID we should validate.
*
* @return boolean True if this session ID is valid, false otherwise.
*/
private static function isValidSessionID($session_id)
{
if (!is_string($session_id)) {
return false;
}
if (strlen($session_id) != 32) {
return false;
}
if (preg_match('/[^0-9a-f]/', $session_id)) {
return false;
}
return true;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
assert(is_string($sessionName));
assert(is_string($sessionID) || $sessionID === null);
if ($cookieParams !== null) {
$params = array_merge($this->getCookieParams(), $cookieParams);
} else {
$params = $this->getCookieParams();
}
HTTP::setCookie($sessionName, $sessionID, $params, true);
}
}

View File

@@ -0,0 +1,362 @@
<?php
/**
* This file is part of SimpleSAMLphp. See the file COPYING in the root of the distribution for licence information.
*
* This file defines a session handler which uses the default php session handler for storage.
*
* @author Olav Morken, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
use SimpleSAML\Error\CannotSetCookie;
use SimpleSAML\Utils\HTTP;
class SessionHandlerPHP extends SessionHandler
{
/**
* This variable contains the session cookie name.
*
* @var string
*/
protected $cookie_name;
/**
* An associative array containing the details of a session existing previously to creating or loading one with this
* session handler. The keys of the array will be:
*
* - id: the ID of the session, as returned by session_id().
* - name: the name of the session, as returned by session_name().
* - cookie_params: the parameters of the session cookie, as returned by session_get_cookie_params().
*
* @var array
*/
private $previous_session = array();
/**
* Initialize the PHP session handling. This constructor is protected because it should only be called from
* \SimpleSAML\SessionHandler::createSessionHandler(...).
*/
protected function __construct()
{
// call the parent constructor in case it should become necessary in the future
parent::__construct();
$config = \SimpleSAML_Configuration::getInstance();
$this->cookie_name = $config->getString('session.phpsession.cookiename', null);
if (session_status() === PHP_SESSION_ACTIVE) {
if (session_name() === $this->cookie_name || $this->cookie_name === null) {
Logger::warning(
'There is already a PHP session with the same name as SimpleSAMLphp\'s session, or the '.
"'session.phpsession.cookiename' configuration option is not set. Make sure to set ".
"SimpleSAMLphp's cookie name with a value not used by any other applications."
);
}
/*
* We shouldn't have a session at this point, so it might be an application session. Save the details to
* retrieve it later and commit.
*/
$this->previous_session['cookie_params'] = session_get_cookie_params();
$this->previous_session['id'] = session_id();
$this->previous_session['name'] = session_name();
session_write_close();
}
if (!empty($this->cookie_name)) {
session_name($this->cookie_name);
} else {
$this->cookie_name = session_name();
}
$params = $this->getCookieParams();
if (!headers_sent()) {
session_set_cookie_params(
$params['lifetime'],
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
$savepath = $config->getString('session.phpsession.savepath', null);
if (!empty($savepath)) {
session_save_path($savepath);
}
}
/**
* This method starts a session, making sure no warnings are generated due to headers being already sent.
*/
private function sessionStart()
{
$cacheLimiter = session_cache_limiter();
if (headers_sent()) {
/*
* session_start() tries to send HTTP headers depending on the configuration, according to the
* documentation:
*
* http://php.net/manual/en/function.session-start.php
*
* If headers have been already sent, it will then trigger an error since no more headers can be sent.
* Being unable to send headers does not mean we cannot recover the session by calling session_start(),
* so we still want to call it. In this case, though, we want to avoid session_start() to send any
* headers at all so that no error is generated, so we clear the cache limiter temporarily (no headers
* sent then) and restore it after successfully starting the session.
*/
session_cache_limiter('');
}
session_cache_limiter($cacheLimiter);
@session_start();
}
/**
* Restore a previously-existing session.
*
* Use this method to restore a previous PHP session existing before SimpleSAMLphp initialized its own session.
*
* WARNING: do not use this method directly, unless you know what you are doing. Calling this method directly,
* outside of SimpleSAML_Session, could cause SimpleSAMLphp's session to be lost or mess the application's one. The
* session must always be saved properly before calling this method. If you don't understand what this is about,
* don't use this method.
*/
public function restorePrevious()
{
if (empty($this->previous_session)) {
return; // nothing to do here
}
// close our own session
session_write_close();
session_name($this->previous_session['name']);
session_set_cookie_params(
$this->previous_session['cookie_params']['lifetime'],
$this->previous_session['cookie_params']['path'],
$this->previous_session['cookie_params']['domain'],
$this->previous_session['cookie_params']['secure'],
$this->previous_session['cookie_params']['httponly']
);
session_id($this->previous_session['id']);
$this->previous_session = array();
$this->sessionStart();
/*
* At this point, we have restored a previously-existing session, so we can't continue to use our session here.
* Therefore, we need to load our session again in case we need it. We remove this handler from the parent
* class so that the handler is initialized again if we ever need to do something with the session.
*/
parent::$sessionHandler = null;
}
/**
* Create a new session id.
*
* @return string The new session id.
*/
public function newSessionId()
{
// generate new (secure) session id
$sessionId = bin2hex(openssl_random_pseudo_bytes(16));
\SimpleSAML_Session::createSession($sessionId);
return $sessionId;
}
/**
* Retrieve the session ID saved in the session cookie, if there's one.
*
* @return string|null The session id saved in the cookie or null if no session cookie was set.
*
* @throws \SimpleSAML_Error_Exception If the cookie is marked as secure but we are not using HTTPS.
*/
public function getCookieSessionId()
{
if (!self::hasSessionCookie()) {
return null; // there's no session cookie, can't return ID
}
// do not rely on session_id() as it can return the ID of a previous session. Get it from the cookie instead.
session_id($_COOKIE[$this->cookie_name]);
$session_cookie_params = session_get_cookie_params();
if ($session_cookie_params['secure'] && !HTTP::isHTTPS()) {
throw new \SimpleSAML_Error_Exception('Session start with secure cookie not allowed on http.');
}
$this->sessionStart();
return session_id();
}
/**
* Retrieve the session cookie name.
*
* @return string The session cookie name.
*/
public function getSessionCookieName()
{
return $this->cookie_name;
}
/**
* Save the current session to the PHP session array.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
public function saveSession(\SimpleSAML_Session $session)
{
$_SESSION['SimpleSAMLphp_SESSION'] = serialize($session);
}
/**
* Load the session from the PHP session array.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*
* @throws \SimpleSAML_Error_Exception If it wasn't possible to disable session cookies or we are trying to load a
* PHP session with a specific identifier and it doesn't match with the current session identifier.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId !== null) {
if (session_id() === '') {
// session not initiated with getCookieSessionId(), start session without setting cookie
$ret = ini_set('session.use_cookies', '0');
if ($ret === false) {
throw new \SimpleSAML_Error_Exception('Disabling PHP option session.use_cookies failed.');
}
session_id($sessionId);
$this->sessionStart();
} elseif ($sessionId !== session_id()) {
throw new \SimpleSAML_Error_Exception('Cannot load PHP session with a specific ID.');
}
} elseif (session_id() === '') {
self::getCookieSessionId();
}
if (!isset($_SESSION['SimpleSAMLphp_SESSION'])) {
return null;
}
$session = $_SESSION['SimpleSAMLphp_SESSION'];
assert(is_string($session));
$session = unserialize($session);
return ($session !== false) ? $session : null;
}
/**
* Check whether the session cookie is set.
*
* This function will only return false if is is certain that the cookie isn't set.
*
* @return boolean True if it was set, false otherwise.
*/
public function hasSessionCookie()
{
return array_key_exists($this->cookie_name, $_COOKIE);
}
/**
* Get the cookie parameters that should be used for session cookies.
*
* This function contains some adjustments from the default to provide backwards-compatibility.
*
* @return array The cookie parameters for our sessions.
* @link http://www.php.net/manual/en/function.session-get-cookie-params.php
*
* @throws \SimpleSAML_Error_Exception If both 'session.phpsession.limitedpath' and 'session.cookie.path' options
* are set at the same time in the configuration.
*/
public function getCookieParams()
{
$config = \SimpleSAML_Configuration::getInstance();
$ret = parent::getCookieParams();
if ($config->hasValue('session.phpsession.limitedpath') && $config->hasValue('session.cookie.path')) {
throw new \SimpleSAML_Error_Exception(
'You cannot set both the session.phpsession.limitedpath and session.cookie.path options.'
);
} elseif ($config->hasValue('session.phpsession.limitedpath')) {
$ret['path'] = $config->getBoolean(
'session.phpsession.limitedpath',
false
) ? $config->getBasePath() : '/';
}
$ret['httponly'] = $config->getBoolean('session.phpsession.httponly', true);
return $ret;
}
/**
* Set a session cookie.
*
* @param string $sessionName The name of the session.
* @param string|null $sessionID The session ID to use. Set to null to delete the cookie.
* @param array|null $cookieParams Additional parameters to use for the session cookie.
*
* @throws \SimpleSAML\Error\CannotSetCookie If we can't set the cookie.
*/
public function setCookie($sessionName, $sessionID, array $cookieParams = null)
{
if ($cookieParams === null) {
$cookieParams = session_get_cookie_params();
}
if ($cookieParams['secure'] && !HTTP::isHTTPS()) {
throw new CannotSetCookie(
'Setting secure cookie on plain HTTP is not allowed.',
CannotSetCookie::SECURE_COOKIE
);
}
if (headers_sent()) {
throw new CannotSetCookie(
'Headers already sent.',
CannotSetCookie::HEADERS_SENT
);
}
session_set_cookie_params(
$cookieParams['lifetime'],
$cookieParams['path'],
$cookieParams['domain'],
$cookieParams['secure'],
$cookieParams['httponly']
);
if (session_id() !== '') {
// session already started, close it
session_write_close();
}
session_id($sessionID);
$this->sessionStart();
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Session storage in the data store.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML;
class SessionHandlerStore extends SessionHandlerCookie
{
/**
* The data store we save the session to.
*
* @var \SimpleSAML\Store
*/
private $store;
/**
* Initialize the session.
*
* @param \SimpleSAML\Store $store The store to use.
*/
protected function __construct(Store $store)
{
parent::__construct();
$this->store = $store;
}
/**
* Load a session from the data store.
*
* @param string|null $sessionId The ID of the session we should load, or null to use the default.
*
* @return \SimpleSAML_Session|null The session object, or null if it doesn't exist.
*/
public function loadSession($sessionId = null)
{
assert(is_string($sessionId) || $sessionId === null);
if ($sessionId === null) {
$sessionId = $this->getCookieSessionId();
if ($sessionId === null) {
// no session cookie, nothing to load
return null;
}
}
$session = $this->store->get('session', $sessionId);
if ($session !== null) {
assert($session instanceof \SimpleSAML_Session);
return $session;
}
return null;
}
/**
* Save a session to the data store.
*
* @param \SimpleSAML_Session $session The session object we should save.
*/
public function saveSession(\SimpleSAML_Session $session)
{
$sessionId = $session->getSessionId();
$config = \SimpleSAML_Configuration::getInstance();
$sessionDuration = $config->getInteger('session.duration', 8 * 60 * 60);
$expire = time() + $sessionDuration;
$this->store->set('session', $sessionId, $session, $expire);
}
}

100
lib/SimpleSAML/Stats.php Executable file
View File

@@ -0,0 +1,100 @@
<?php
/**
* Statistics handler class.
*
* This class is responsible for taking a statistics event and logging it.
*
* @package SimpleSAMLphp
*/
class SimpleSAML_Stats
{
/**
* Whether this class is initialized.
*
* @var boolean
*/
private static $initialized = false;
/**
* The statistics output callbacks.
*
* @var array
*/
private static $outputs = null;
/**
* Create an output from a configuration object.
*
* @param SimpleSAML_Configuration $config The configuration object.
*
* @return mixed A new instance of the configured class.
*/
private static function createOutput(SimpleSAML_Configuration $config)
{
$cls = $config->getString('class');
$cls = SimpleSAML\Module::resolveClass($cls, 'Stats_Output', 'SimpleSAML_Stats_Output');
$output = new $cls($config);
return $output;
}
/**
* Initialize the outputs.
*/
private static function initOutputs()
{
$config = SimpleSAML_Configuration::getInstance();
$outputCfgs = $config->getConfigList('statistics.out', array());
self::$outputs = array();
foreach ($outputCfgs as $cfg) {
self::$outputs[] = self::createOutput($cfg);
}
}
/**
* Notify about an event.
*
* @param string $event The event.
* @param array $data Event data. Optional.
*
* @return void|boolean False if output is not enabled, void otherwise.
*/
public static function log($event, array $data = array())
{
assert(is_string($event));
assert(!isset($data['op']));
assert(!isset($data['time']));
assert(!isset($data['_id']));
if (!self::$initialized) {
self::initOutputs();
self::$initialized = true;
}
if (empty(self::$outputs)) {
// not enabled
return;
}
$data['op'] = $event;
$data['time'] = microtime(true);
// the ID generation is designed to cluster IDs related in time close together
$int_t = (int) $data['time'];
$hd = openssl_random_pseudo_bytes(16);
$data['_id'] = sprintf('%016x%s', $int_t, bin2hex($hd));
foreach (self::$outputs as $out) {
$out->emit($data);
}
}
}

29
lib/SimpleSAML/Stats/Output.php Executable file
View File

@@ -0,0 +1,29 @@
<?php
/**
* Interface for statistics outputs.
*
* @package SimpleSAMLphp
*/
abstract class SimpleSAML_Stats_Output
{
/**
* Initialize the output.
*
* @param SimpleSAML_Configuration $config The configuration for this output.
*/
public function __construct(SimpleSAML_Configuration $config)
{
// do nothing by default
}
/**
* Write a stats event.
*
* @param array $data The event.
*/
abstract public function emit(array $data);
}

106
lib/SimpleSAML/Store.php Executable file
View File

@@ -0,0 +1,106 @@
<?php
namespace SimpleSAML;
use SimpleSAML\Error\CriticalConfigurationError;
/**
* Base class for data stores.
*
* @package SimpleSAMLphp
*/
abstract class Store
{
/**
* Our singleton instance.
*
* This is false if the data store isn't enabled, and null if we haven't attempted to initialize it.
*
* @var \SimpleSAML\Store|false|null
*/
private static $instance;
/**
* Retrieve our singleton instance.
*
* @return false|\SimpleSAML\Store The data store, or false if it isn't enabled.
*
* @throws \SimpleSAML\Error\CriticalConfigurationError
*/
public static function getInstance()
{
if (self::$instance !== null) {
return self::$instance;
}
$config = \SimpleSAML_Configuration::getInstance();
$storeType = $config->getString('store.type', null);
if ($storeType === null) {
$storeType = $config->getString('session.handler', 'phpsession');
}
switch ($storeType) {
case 'phpsession':
// we cannot support advanced features with the PHP session store
self::$instance = false;
break;
case 'memcache':
self::$instance = new Store\Memcache();
break;
case 'sql':
self::$instance = new Store\SQL();
break;
case 'redis':
self::$instance = new Store\Redis();
break;
default:
// datastore from module
try {
$className = Module::resolveClass($storeType, 'Store', '\SimpleSAML\Store');
} catch (\Exception $e) {
$c = $config->toArray();
$c['store.type'] = 'phpsession';
throw new CriticalConfigurationError(
"Invalid 'store.type' configuration option. Cannot find store '$storeType'.",
null,
$c
);
}
self::$instance = new $className();
}
return self::$instance;
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*
* @return mixed|null The value.
*/
abstract public function get($type, $key);
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
abstract public function set($type, $key, $value, $expire = null);
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*/
abstract public function delete($type, $key);
}

View File

@@ -0,0 +1,84 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Store;
/**
* A memcache based data store.
*
* @package SimpleSAMLphp
*/
class Memcache extends Store
{
/**
* This variable contains the session name prefix.
*
* @var string
*/
private $prefix;
/**
* This function implements the constructor for this class. It loads the Memcache configuration.
*/
protected function __construct()
{
$config = Configuration::getInstance();
$this->prefix = $config->getString('memcache_store.prefix', 'simpleSAMLphp');
}
/**
* Retrieve a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @return mixed|null The value.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
return \SimpleSAML_Memcache::get($this->prefix . '.' . $type . '.' . $key);
}
/**
* Save a value to the data store.
*
* @param string $type The data type.
* @param string $key The key.
* @param mixed $value The value.
* @param int|NULL $expire The expiration time (unix timestamp), or NULL if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if ($expire === null) {
$expire = 0;
}
\SimpleSAML_Memcache::set($this->prefix . '.' . $type . '.' . $key, $value, $expire);
}
/**
* Delete a value from the data store.
*
* @param string $type The data type.
* @param string $key The key.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
\SimpleSAML_Memcache::delete($this->prefix . '.' . $type . '.' . $key);
}
}

119
lib/SimpleSAML/Store/Redis.php Executable file
View File

@@ -0,0 +1,119 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Store;
/**
* A data store using Redis to keep the data.
*
* @package SimpleSAMLphp
*/
class Redis extends Store
{
public $redis;
/**
* Initialize the Redis data store.
*/
public function __construct($redis = null)
{
assert($redis === null || is_subclass_of($redis, 'Predis\\Client'));
if (!class_exists('\Predis\Client')) {
throw new \SimpleSAML\Error\CriticalConfigurationError('predis/predis is not available.');
}
if ($redis === null) {
$config = Configuration::getInstance();
$host = $config->getString('store.redis.host', 'localhost');
$port = $config->getInteger('store.redis.port', 6379);
$prefix = $config->getString('store.redis.prefix', 'SimpleSAMLphp');
$redis = new \Predis\Client(
array(
'scheme' => 'tcp',
'host' => $host,
'port' => $port,
),
array(
'prefix' => $prefix,
)
);
}
$this->redis = $redis;
}
/**
* Deconstruct the Redis data store.
*/
public function __destruct()
{
if (method_exists($this->redis, 'disconnect')) {
$this->redis->disconnect();
}
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$result = $this->redis->get("{$type}.{$key}");
if ($result === false || $result === null) {
return null;
}
return unserialize($result);
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
$serialized = serialize($value);
if ($expire === null) {
$this->redis->set("{$type}.{$key}", $serialized);
} else {
// setex expire time is in seconds (not unix timestamp)
$this->redis->setex("{$type}.{$key}", $expire - time(), $serialized);
}
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
$this->redis->del("{$type}.{$key}");
}
}

389
lib/SimpleSAML/Store/SQL.php Executable file
View File

@@ -0,0 +1,389 @@
<?php
namespace SimpleSAML\Store;
use \SimpleSAML_Configuration as Configuration;
use \SimpleSAML\Logger;
use \SimpleSAML\Store;
/**
* A data store using a RDBMS to keep the data.
*
* @package SimpleSAMLphp
*/
class SQL extends Store
{
/**
* The PDO object for our database.
*
* @var \PDO
*/
public $pdo;
/**
* Our database driver.
*
* @var string
*/
public $driver;
/**
* The prefix we should use for our tables.
*
* @var string
*/
public $prefix;
/**
* Associative array of table versions.
*
* @var array
*/
private $tableVersions;
/**
* Initialize the SQL data store.
*/
public function __construct()
{
$config = Configuration::getInstance();
$dsn = $config->getString('store.sql.dsn');
$username = $config->getString('store.sql.username', null);
$password = $config->getString('store.sql.password', null);
$options = $config->getArray('store.sql.options', null);
$this->prefix = $config->getString('store.sql.prefix', 'simpleSAMLphp');
try {
$this->pdo = new \PDO($dsn, $username, $password, $options);
} catch (\PDOException $e) {
throw new \Exception("Database error: " . $e->getMessage());
}
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
if ($this->driver === 'mysql') {
$this->pdo->exec('SET time_zone = "+00:00"');
}
$this->initTableVersionTable();
$this->initKVTable();
}
/**
* Initialize the table-version table.
*/
private function initTableVersionTable()
{
$this->tableVersions = array();
try {
$fetchTableVersion = $this->pdo->query('SELECT _name, _version FROM '.$this->prefix.'_tableVersion');
} catch (\PDOException $e) {
$this->pdo->exec(
'CREATE TABLE '.$this->prefix.
'_tableVersion (_name VARCHAR(30) NOT NULL UNIQUE, _version INTEGER NOT NULL)'
);
return;
}
while (($row = $fetchTableVersion->fetch(\PDO::FETCH_ASSOC)) !== false) {
$this->tableVersions[$row['_name']] = (int) $row['_version'];
}
}
/**
* Initialize key-value table.
*/
private function initKVTable()
{
$current_version = $this->getTableVersion('kvstore');
$text_t = 'TEXT';
if ($this->driver === 'mysql') {
// TEXT data type has size constraints that can be hit at some point, so we use LONGTEXT instead
$text_t = 'LONGTEXT';
}
/**
* Queries for updates, grouped by version.
* New updates can be added as a new array in this array
*/
$table_updates = array(
array(
'CREATE TABLE '.$this->prefix.
'_kvstore (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value '.$text_t.
' NOT NULL, _expire TIMESTAMP, PRIMARY KEY (_key, _type))',
'CREATE INDEX '.$this->prefix.'_kvstore_expire ON '.$this->prefix.'_kvstore (_expire)'
),
/**
* This upgrade removes the default NOT NULL constraint on the _expire field in MySQL.
* Because SQLite does not support field alterations, the approach is to:
* Create a new table without the NOT NULL constraint
* Copy the current data to the new table
* Drop the old table
* Rename the new table correctly
* Readd the index
*/
array(
'CREATE TABLE '.$this->prefix.
'_kvstore_new (_type VARCHAR(30) NOT NULL, _key VARCHAR(50) NOT NULL, _value '.$text_t.
' NOT NULL, _expire TIMESTAMP NULL, PRIMARY KEY (_key, _type))',
'INSERT INTO '.$this->prefix.'_kvstore_new SELECT * FROM ' . $this->prefix.'_kvstore',
'DROP TABLE '.$this->prefix.'_kvstore',
'ALTER TABLE '.$this->prefix.'_kvstore_new RENAME TO ' . $this->prefix . '_kvstore',
'CREATE INDEX '.$this->prefix.'_kvstore_expire ON '.$this->prefix.'_kvstore (_expire)'
)
);
$latest_version = count($table_updates);
if ($current_version == $latest_version) {
return;
}
// Only run queries for after the current version
$updates_to_run = array_slice($table_updates, $current_version);
foreach ($updates_to_run as $version_updates) {
foreach ($version_updates as $query) {
$this->pdo->exec($query);
}
}
$this->setTableVersion('kvstore', $latest_version);
}
/**
* Get table version.
*
* @param string $name Table name.
*
* @return int The table version, or 0 if the table doesn't exist.
*/
public function getTableVersion($name)
{
assert(is_string($name));
if (!isset($this->tableVersions[$name])) {
return 0;
}
return $this->tableVersions[$name];
}
/**
* Set table version.
*
* @param string $name Table name.
* @param int $version Table version.
*/
public function setTableVersion($name, $version)
{
assert(is_string($name));
assert(is_int($version));
$this->insertOrUpdate(
$this->prefix.'_tableVersion',
array('_name'),
array('_name' => $name, '_version' => $version)
);
$this->tableVersions[$name] = $version;
}
/**
* Insert or update a key-value in the store.
*
* Since various databases implement different methods for doing this, we abstract it away here.
*
* @param string $table The table we should update.
* @param array $keys The key columns.
* @param array $data Associative array with columns.
*/
public function insertOrUpdate($table, array $keys, array $data)
{
assert(is_string($table));
$colNames = '('.implode(', ', array_keys($data)).')';
$values = 'VALUES(:'.implode(', :', array_keys($data)).')';
switch ($this->driver) {
case 'mysql':
$query = 'REPLACE INTO '.$table.' '.$colNames.' '.$values;
$query = $this->pdo->prepare($query);
$query->execute($data);
return;
case 'sqlite':
$query = 'INSERT OR REPLACE INTO '.$table.' '.$colNames.' '.$values;
$query = $this->pdo->prepare($query);
$query->execute($data);
return;
}
// default implementation, try INSERT, and UPDATE if that fails.
$insertQuery = 'INSERT INTO '.$table.' '.$colNames.' '.$values;
$insertQuery = $this->pdo->prepare($insertQuery);
try {
$insertQuery->execute($data);
return;
} catch (\PDOException $e) {
$ecode = (string) $e->getCode();
switch ($ecode) {
case '23505': // PostgreSQL
break;
default:
Logger::error('Error while saving data: '.$e->getMessage());
throw $e;
}
}
$updateCols = array();
$condCols = array();
foreach ($data as $col => $value) {
$tmp = $col.' = :'.$col;
if (in_array($col, $keys, true)) {
$condCols[] = $tmp;
} else {
$updateCols[] = $tmp;
}
}
$updateQuery = 'UPDATE '.$table.' SET '.implode(',', $updateCols).' WHERE '.implode(' AND ', $condCols);
$updateQuery = $this->pdo->prepare($updateQuery);
$updateQuery->execute($data);
}
/**
* Clean the key-value table of expired entries.
*/
private function cleanKVStore()
{
Logger::debug('store.sql: Cleaning key-value store.');
$query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _expire < :now';
$params = array('now' => gmdate('Y-m-d H:i:s'));
$query = $this->pdo->prepare($query);
$query->execute($params);
}
/**
* Retrieve a value from the data store.
*
* @param string $type The type of the data.
* @param string $key The key to retrieve.
*
* @return mixed|null The value associated with that key, or null if there's no such key.
*/
public function get($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$query = 'SELECT _value FROM '.$this->prefix.
'_kvstore WHERE _type = :type AND _key = :key AND (_expire IS NULL OR _expire > :now)';
$params = array('type' => $type, 'key' => $key, 'now' => gmdate('Y-m-d H:i:s'));
$query = $this->pdo->prepare($query);
$query->execute($params);
$row = $query->fetch(\PDO::FETCH_ASSOC);
if ($row === false) {
return null;
}
$value = $row['_value'];
if (is_resource($value)) {
$value = stream_get_contents($value);
}
$value = urldecode($value);
$value = unserialize($value);
if ($value === false) {
return null;
}
return $value;
}
/**
* Save a value in the data store.
*
* @param string $type The type of the data.
* @param string $key The key to insert.
* @param mixed $value The value itself.
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
*/
public function set($type, $key, $value, $expire = null)
{
assert(is_string($type));
assert(is_string($key));
assert($expire === null || (is_int($expire) && $expire > 2592000));
if (rand(0, 1000) < 10) {
$this->cleanKVStore();
}
if (strlen($key) > 50) {
$key = sha1($key);
}
if ($expire !== null) {
$expire = gmdate('Y-m-d H:i:s', $expire);
}
$value = serialize($value);
$value = rawurlencode($value);
$data = array(
'_type' => $type,
'_key' => $key,
'_value' => $value,
'_expire' => $expire,
);
$this->insertOrUpdate($this->prefix.'_kvstore', array('_type', '_key'), $data);
}
/**
* Delete an entry from the data store.
*
* @param string $type The type of the data
* @param string $key The key to delete.
*/
public function delete($type, $key)
{
assert(is_string($type));
assert(is_string($key));
if (strlen($key) > 50) {
$key = sha1($key);
}
$data = array(
'_type' => $type,
'_key' => $key,
);
$query = 'DELETE FROM '.$this->prefix.'_kvstore WHERE _type=:_type AND _key=:_key';
$query = $this->pdo->prepare($query);
$query->execute($data);
}
}

707
lib/SimpleSAML/Utilities.php Executable file
View File

@@ -0,0 +1,707 @@
<?php
/**
* Misc static functions that is used several places.in example parsing and id generation.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*
* @deprecated This entire class will be removed in SimpleSAMLphp 2.0.
*/
class SimpleSAML_Utilities
{
/**
* @deprecated This property will be removed in SSP 2.0. Please use SimpleSAML\Logger::isErrorMasked() instead.
*/
public static $logMask = 0;
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHost() instead.
*/
public static function getSelfHost()
{
return \SimpleSAML\Utils\HTTP::getSelfHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLHost() instead.
*/
public static function selfURLhost()
{
return \SimpleSAML\Utils\HTTP::getSelfURLHost();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::isHTTPS() instead.
*/
public static function isHTTPS()
{
return \SimpleSAML\Utils\HTTP::isHTTPS();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURLNoQuery()
* instead.
*/
public static function selfURLNoQuery()
{
return \SimpleSAML\Utils\HTTP::getSelfURLNoQuery();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfHostWithPath()
* instead.
*/
public static function getSelfHostWithPath()
{
return \SimpleSAML\Utils\HTTP::getSelfHostWithPath();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getFirstPathElement()
* instead.
*/
public static function getFirstPathElement($trailingslash = true)
{
return \SimpleSAML\Utils\HTTP::getFirstPathElement($trailingslash);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getSelfURL() instead.
*/
public static function selfURL()
{
return \SimpleSAML\Utils\HTTP::getSelfURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getBaseURL() instead.
*/
public static function getBaseURL()
{
return \SimpleSAML\Utils\HTTP::getBaseURL();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::addURLParameters() instead.
*/
public static function addURLparameter($url, $parameters)
{
return \SimpleSAML\Utils\HTTP::addURLParameters($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\HTTP::checkURLAllowed() instead.
*/
public static function checkURLAllowed($url, array $trustedSites = null)
{
return \SimpleSAML\Utils\HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML_Auth_State::parseStateID() instead.
*/
public static function parseStateID($stateId)
{
return SimpleSAML_Auth_State::parseStateID($stateId);
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function checkDateConditions($start = null, $end = null)
{
$currentTime = time();
if (!empty($start)) {
$startTime = \SAML2\Utils::xsDateTimeToTimestamp($start);
// Allow for a 10 minute difference in Time
if (($startTime < 0) || (($startTime - 600) > $currentTime)) {
return false;
}
}
if (!empty($end)) {
$endTime = \SAML2\Utils::xsDateTimeToTimestamp($end);
if (($endTime < 0) || ($endTime <= $currentTime)) {
return false;
}
}
return true;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Random::generateID() instead.
*/
public static function generateID()
{
return SimpleSAML\Utils\Random::generateID();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::generateTimestamp()
* instead.
*/
public static function generateTimestamp($instant = null)
{
return SimpleSAML\Utils\Time::generateTimestamp($instant);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Utils\Time::parseDuration() instead.
*/
public static function parseDuration($duration, $timestamp = null)
{
return SimpleSAML\Utils\Time::parseDuration($duration, $timestamp);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please raise a SimpleSAML_Error_Error exception instead.
*/
public static function fatalError($trackId = 'na', $errorCode = null, Exception $e = null)
{
throw new SimpleSAML_Error_Error($errorCode, $e);
}
/**
* @deprecated This method will be removed in version 2.0. Use SimpleSAML\Utils\Net::ipCIDRcheck() instead.
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
return SimpleSAML\Utils\Net::ipCIDRcheck($cidr, $ip);
}
private static function _doRedirect($url, $parameters = array())
{
assert(is_string($url));
assert(!empty($url));
assert(is_array($parameters));
if (!empty($parameters)) {
$url = self::addURLparameter($url, $parameters);
}
/* Set the HTTP result code. This is either 303 See Other or
* 302 Found. HTTP 303 See Other is sent if the HTTP version
* is HTTP/1.1 and the request type was a POST request.
*/
if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' &&
$_SERVER['REQUEST_METHOD'] === 'POST'
) {
$code = 303;
} else {
$code = 302;
}
if (strlen($url) > 2048) {
SimpleSAML\Logger::warning('Redirecting to a URL longer than 2048 bytes.');
}
// Set the location header
header('Location: '.$url, true, $code);
// Disable caching of this response
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
// Show a minimal web page with a clickable link to the URL
echo '<?xml version="1.0" encoding="UTF-8"?>'."\n";
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'.
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n";
echo '<html xmlns="http://www.w3.org/1999/xhtml">';
echo '<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Redirect</title>
</head>';
echo '<body>';
echo '<h1>Redirect</h1>';
echo '<p>';
echo 'You were redirected to: ';
echo '<a id="redirlink" href="'.
htmlspecialchars($url).'">'.htmlspecialchars($url).'</a>';
echo '<script type="text/javascript">document.getElementById("redirlink").focus();</script>';
echo '</p>';
echo '</body>';
echo '</html>';
// End script execution
exit;
}
/**
* @deprecated 1.12.0 This method will be removed from the API. Instead, use the redirectTrustedURL() or
* redirectUntrustedURL() functions accordingly.
*/
public static function redirect($url, $parameters = array(), $allowed_redirect_hosts = null)
{
assert(is_string($url));
assert(strlen($url) > 0);
assert(is_array($parameters));
if ($allowed_redirect_hosts !== null) {
$url = self::checkURLAllowed($url, $allowed_redirect_hosts);
} else {
$url = self::normalizeURL($url);
}
self::_doRedirect($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectTrustedURL()
* instead.
*/
public static function redirectTrustedURL($url, $parameters = array())
{
\SimpleSAML\Utils\HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::redirectUntrustedURL()
* instead.
*/
public static function redirectUntrustedURL($url, $parameters = array())
{
\SimpleSAML\Utils\HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::transpose() instead.
*/
public static function transposeArray($in)
{
return SimpleSAML\Utils\Arrays::transpose($in);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isDOMNodeOfType()
* instead.
*/
public static function isDOMElementOfType(DOMNode $element, $name, $nsURI)
{
return SimpleSAML\Utils\XML::isDOMNodeOfType($element, $name, $nsURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMChildren() instead.
*/
public static function getDOMChildren(DOMElement $element, $localName, $namespaceURI)
{
return SimpleSAML\Utils\XML::getDOMChildren($element, $localName, $namespaceURI);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::getDOMText() instead.
*/
public static function getDOMText($element)
{
return SimpleSAML\Utils\XML::getDOMText($element);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getAcceptLanguage()
* instead.
*/
public static function getAcceptLanguage()
{
return \SimpleSAML\Utils\HTTP::getAcceptLanguage();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::isValid() instead.
*/
public static function validateXML($xml, $schema)
{
$result = \SimpleSAML\Utils\XML::isValid($xml, $schema);
return ($result === true) ? '' : $result;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::checkSAMLMessage() instead.
*/
public static function validateXMLDocument($message, $type)
{
\SimpleSAML\Utils\XML::checkSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use openssl_random_pseudo_bytes() instead.
*/
public static function generateRandomBytes($length)
{
assert(is_int($length));
return openssl_random_pseudo_bytes($length);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use bin2hex() instead.
*/
public static function stringToHex($bytes)
{
$ret = '';
for ($i = 0; $i < strlen($bytes); $i++) {
$ret .= sprintf('%02x', ord($bytes[$i]));
}
return $ret;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::resolvePath() instead.
*/
public static function resolvePath($path, $base = null)
{
return \SimpleSAML\Utils\System::resolvePath($path, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::resolveURL() instead.
*/
public static function resolveURL($url, $base = null)
{
return \SimpleSAML\Utils\HTTP::resolveURL($url, $base);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::normalizeURL() instead.
*/
public static function normalizeURL($url)
{
return \SimpleSAML\Utils\HTTP::normalizeURL($url);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::parseQueryString() instead.
*/
public static function parseQueryString($query_string)
{
return \SimpleSAML\Utils\HTTP::parseQueryString($query_string);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Attributes::normalizeAttributesArray() instead.
*/
public static function parseAttributes($attributes)
{
return SimpleSAML\Utils\Attributes::normalizeAttributesArray($attributes);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getSecretSalt() instead.
*/
public static function getSecretSalt()
{
return SimpleSAML\Utils\Config::getSecretSalt();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please call error_get_last() directly.
*/
public static function getLastError()
{
if (!function_exists('error_get_last')) {
return '[Cannot get error message]';
}
$error = error_get_last();
if ($error === null) {
return '[No error message found]';
}
return $error['message'];
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Config::getCertPath() instead.
*/
public static function resolveCert($path)
{
return \SimpleSAML\Utils\Config::getCertPath($path);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPublicKey() instead.
*/
public static function loadPublicKey(SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
return SimpleSAML\Utils\Crypto::loadPublicKey($metadata, $required, $prefix);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::loadPrivateKey() instead.
*/
public static function loadPrivateKey(SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
return SimpleSAML\Utils\Crypto::loadPrivateKey($metadata, $required, $prefix);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatDOMElement() instead.
*/
public static function formatDOMElement(DOMElement $root, $indentBase = '')
{
SimpleSAML\Utils\XML::formatDOMElement($root, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::formatXMLString() instead.
*/
public static function formatXMLString($xml, $indentBase = '')
{
return SimpleSAML\Utils\XML::formatXMLString($xml, $indentBase);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Arrays::arrayize() instead.
*/
public static function arrayize($data, $index = 0)
{
return SimpleSAML\Utils\Arrays::arrayize($data, $index);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::isAdmin() instead.
*/
public static function isAdmin()
{
return SimpleSAML\Utils\Auth::isAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::getAdminLoginURL instead();
*/
public static function getAdminLoginURL($returnTo = null)
{
return SimpleSAML\Utils\Auth::getAdminLoginURL($returnTo);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Auth::requireAdmin() instead.
*/
public static function requireAdmin()
{
\SimpleSAML\Utils\Auth::requireAdmin();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::submitPOSTData() instead.
*/
public static function postRedirect($destination, $post)
{
\SimpleSAML\Utils\HTTP::submitPOSTData($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. PLease use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
*/
public static function createPostRedirectLink($destination, $post)
{
return \SimpleSAML\Utils\HTTP::getPOSTRedirectURL($destination, $post);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::getPOSTRedirectURL()
* instead.
*/
public static function createHttpPostRedirectLink($destination, $post)
{
assert(is_string($destination));
assert(is_array($post));
$postId = SimpleSAML\Utils\Random::generateID();
$postData = array(
'post' => $post,
'url' => $destination,
);
$session = SimpleSAML_Session::getSessionFromRequest();
$session->setData('core_postdatalink', $postId, $postData);
$redirInfo = base64_encode(SimpleSAML\Utils\Crypto::aesEncrypt($session->getSessionId().':'.$postId));
$url = SimpleSAML\Module::getModuleURL('core/postredirect.php', array('RedirInfo' => $redirInfo));
$url = preg_replace("#^https:#", "http:", $url);
return $url;
}
/**
* @deprecated This method will be removed in SSP 2.0.
*/
public static function validateCA($certificate, $caFile)
{
\SimpleSAML\XML\Validator::validateCertificate($certificate, $caFile);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Time::initTimezone() instead.
*/
public static function initTimezone()
{
\SimpleSAML\Utils\Time::initTimezone();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::writeFile() instead.
*/
public static function writeFile($filename, $data, $mode = 0600)
{
\SimpleSAML\Utils\System::writeFile($filename, $data, $mode);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getTempDir instead.
*/
public static function getTempDir()
{
return SimpleSAML\Utils\System::getTempDir();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::maskErrors() instead.
*/
public static function maskErrors($mask)
{
SimpleSAML\Logger::maskErrors($mask);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Logger::popErrorMask() instead.
*/
public static function popErrorMask()
{
SimpleSAML\Logger::popErrorMask();
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use
* SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint() instead.
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
return \SimpleSAML\Utils\Config\Metadata::getDefaultEndpoint($endpoints, $bindings);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::checkSessionCookie()
* instead.
*/
public static function checkCookie($retryURL = null)
{
\SimpleSAML\Utils\HTTP::checkSessionCookie($retryURL);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\XML::debugSAMLMessage() instead.
*/
public static function debugMessage($message, $type)
{
\SimpleSAML\Utils\XML::debugSAMLMessage($message, $type);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::fetch() instead.
*/
public static function fetch($path, $context = array(), $getHeaders = false)
{
return \SimpleSAML\Utils\HTTP::fetch($path, $context, $getHeaders);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesEncrypt() instead.
*/
public static function aesEncrypt($clear)
{
return SimpleSAML\Utils\Crypto::aesEncrypt($clear);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\Crypto::aesDecrypt() instead.
*/
public static function aesDecrypt($encData)
{
return SimpleSAML\Utils\Crypto::aesDecrypt($encData);
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\System::getOS() instead.
*/
public static function isWindowsOS()
{
return SimpleSAML\Utils\System::getOS() === SimpleSAML\Utils\System::WINDOWS;
}
/**
* @deprecated This method will be removed in SSP 2.0. Please use SimpleSAML\Utils\HTTP::setCookie() instead.
*/
public static function setCookie($name, $value, array $params = null, $throw = true)
{
\SimpleSAML\Utils\HTTP::setCookie($name, $value, $params, $throw);
}
}

60
lib/SimpleSAML/Utils/Arrays.php Executable file
View File

@@ -0,0 +1,60 @@
<?php
namespace SimpleSAML\Utils;
/**
* Array-related utility methods.
*
* @package SimpleSAMLphp
*/
class Arrays
{
/**
* Put a non-array variable into an array.
*
* @param mixed $data The data to place into an array.
* @param mixed $index The index or key of the array where to place the data. Defaults to 0.
*
* @return array An array with one element containing $data, with key $index, or $data itself if it's already an
* array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function arrayize($data, $index = 0)
{
return (is_array($data)) ? $data : array($index => $data);
}
/**
* This function transposes a two-dimensional array, so that $a['k1']['k2'] becomes $a['k2']['k1'].
*
* @param array $array The two-dimensional array to transpose.
*
* @return mixed The transposed array, or false if $array is not a valid two-dimensional array.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
*/
public static function transpose($array)
{
if (!is_array($array)) {
return false;
}
$ret = array();
foreach ($array as $k1 => $a2) {
if (!is_array($a2)) {
return false;
}
foreach ($a2 as $k2 => $v) {
if (!array_key_exists($k2, $ret)) {
$ret[$k2] = array();
}
$ret[$k2][$k1] = $v;
}
}
return $ret;
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace SimpleSAML\Utils;
/**
* Attribute-related utility methods.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
* @package SimpleSAML
*/
class Attributes
{
/**
* Look for an attribute in a normalized attributes array, failing if it's not there.
*
* @param array $attributes The normalized array containing attributes.
* @param string $expected The name of the attribute we are looking for.
* @param bool $allow_multiple Whether to allow multiple values in the attribute or not.
*
* @return mixed The value of the attribute we are expecting. If the attribute has multiple values and
* $allow_multiple is set to true, the first value will be returned.
*
* @throws \InvalidArgumentException If $attributes is not an array or $expected is not a string.
* @throws \SimpleSAML_Error_Exception If the expected attribute was not found in the attributes array.
*/
public static function getExpectedAttribute($attributes, $expected, $allow_multiple = false)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'.'
);
}
if (!is_string($expected)) {
throw new \InvalidArgumentException(
'The expected attribute is not a string, it is: '.print_r($expected, true).'.'
);
}
if (!array_key_exists($expected, $attributes)) {
throw new \SimpleSAML_Error_Exception("No such attribute '".$expected."' found.");
}
$attribute = $attributes[$expected];
if (!is_array($attribute)) {
throw new \InvalidArgumentException('The attributes array is not normalized, values should be arrays.');
}
if (count($attribute) === 0) {
throw new \SimpleSAML_Error_Exception("Empty attribute '".$expected."'.'");
} elseif (count($attribute) > 1) {
if ($allow_multiple === false) {
throw new \SimpleSAML_Error_Exception(
'More than one value found for the attribute, multiple values not allowed.'
);
}
}
return reset($attribute);
}
/**
* Validate and normalize an array with attributes.
*
* This function takes in an associative array with attributes, and parses and validates
* this array. On success, it will return a normalized array, where each attribute name
* is an index to an array of one or more strings. On failure an exception will be thrown.
* This exception will contain an message describing what is wrong.
*
* @param array $attributes The array containing attributes that we should validate and normalize.
*
* @return array The normalized attributes array.
* @throws \InvalidArgumentException If input is not an array, array keys are not strings or attribute values are
* not strings.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function normalizeAttributesArray($attributes)
{
if (!is_array($attributes)) {
throw new \InvalidArgumentException(
'The attributes array is not an array, it is: '.print_r($attributes, true).'".'
);
}
$newAttrs = array();
foreach ($attributes as $name => $values) {
if (!is_string($name)) {
throw new \InvalidArgumentException('Invalid attribute name: "'.print_r($name, true).'".');
}
$values = Arrays::arrayize($values);
foreach ($values as $value) {
if (!is_string($value)) {
throw new \InvalidArgumentException(
'Invalid attribute value for attribute '.$name.': "'.print_r($value, true).'".'
);
}
}
$newAttrs[$name] = $values;
}
return $newAttrs;
}
/**
* Extract an attribute's namespace, or revert to default.
*
* This function takes in a namespaced attribute name and splits it in a namespace/attribute name tuple.
* When no namespace is found in the attribute name, it will be namespaced with the default namespace.
* This default namespace can be overriden by supplying a second parameter to this function.
*
* @param string $name The namespaced attribute name.
* @param string $defaultns The default namespace that should be used when no namespace is found.
*
* @return array The attribute name, split to the namespace and the actual attribute name.
*/
public static function getAttributeNamespace($name, $defaultns)
{
$slash = strrpos($name, '/');
if ($slash !== false) {
$defaultns = substr($name, 0, $slash);
$name = substr($name, $slash + 1);
}
return array(htmlspecialchars($defaultns), htmlspecialchars($name));
}
}

76
lib/SimpleSAML/Utils/Auth.php Executable file
View File

@@ -0,0 +1,76 @@
<?php
namespace SimpleSAML\Utils;
use SimpleSAML\Module;
/**
* Auth-related utility methods.
*
* @package SimpleSAMLphp
*/
class Auth
{
/**
* Retrieve a admin login URL.
*
* @param string|NULL $returnTo The URL the user should arrive on after admin authentication. Defaults to null.
*
* @return string A URL which can be used for admin authentication.
* @throws \InvalidArgumentException If $returnTo is neither a string nor null.
*/
public static function getAdminLoginURL($returnTo = null)
{
if (!(is_string($returnTo) || is_null($returnTo))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
if ($returnTo === null) {
$returnTo = HTTP::getSelfURL();
}
return Module::getModuleURL('core/login-admin.php', array('ReturnTo' => $returnTo));
}
/**
* Check whether the current user is admin.
*
* @return boolean True if the current user is an admin user, false otherwise.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isAdmin()
{
$session = \SimpleSAML_Session::getSessionFromRequest();
return $session->isValid('admin') || $session->isValid('login-admin');
}
/**
* Require admin access to the current page.
*
* This is a helper function for limiting a page to those with administrative access. It will redirect the user to
* a login page if the current user doesn't have admin access.
*
* @return void This function will only return if the user is admin.
* @throws \SimpleSAML_Error_Exception If no "admin" authentication source was configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function requireAdmin()
{
if (self::isAdmin()) {
return;
}
// not authenticated as admin user, start authentication
if (\SimpleSAML_Auth_Source::getById('admin') !== null) {
$as = new \SimpleSAML\Auth\Simple('admin');
$as->login();
} else {
throw new \SimpleSAML_Error_Exception(
'Cannot find "admin" auth source, and admin privileges are required.'
);
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace SimpleSAML\Utils;
/**
* Indicates an implementation caches state internally and may be cleared.
*
* Primarily designed to allow SSP state to be cleared between unit tests.
* @package SimpleSAML\Utils
*/
interface ClearableState
{
/**
* Clear any cached internal state.
*/
public static function clearInternalState();
}

92
lib/SimpleSAML/Utils/Config.php Executable file
View File

@@ -0,0 +1,92 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for SimpleSAMLphp configuration management and manipulation.
*
* @package SimpleSAMLphp
*/
class Config
{
/**
* Resolves a path that may be relative to the cert-directory.
*
* @param string $path The (possibly relative) path to the file.
*
* @return string The file path.
* @throws \InvalidArgumentException If $path is not a string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getCertPath($path)
{
if (!is_string($path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$base = $globalConfig->getPathValue('certdir', 'cert/');
return System::resolvePath($path, $base);
}
/**
* Retrieve the secret salt.
*
* This function retrieves the value which is configured as the secret salt. It will check that the value exists
* and is set to a non-default value. If it isn't, an exception will be thrown.
*
* The secret salt can be used as a component in hash functions, to make it difficult to test all possible values
* in order to retrieve the original value. It can also be used as a simple method for signing data, by hashing the
* data together with the salt.
*
* @return string The secret salt.
* @throws \InvalidArgumentException If the secret salt hasn't been configured.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getSecretSalt()
{
$secretSalt = \SimpleSAML_Configuration::getInstance()->getString('secretsalt');
if ($secretSalt === 'defaultsecretsalt') {
throw new \InvalidArgumentException('The "secretsalt" configuration option must be set to a secret value.');
}
return $secretSalt;
}
/**
* Returns the path to the config dir
*
* If the SIMPLESAMLPHP_CONFIG_DIR environment variable has been set, it takes precedence over the default
* $simplesamldir/config directory.
*
* @return string The path to the configuration directory.
*/
public static function getConfigDir()
{
$configDir = dirname(dirname(dirname(__DIR__))) . '/config';
/** @var string|false $configDirEnv */
$configDirEnv = getenv('SIMPLESAMLPHP_CONFIG_DIR');
if($configDirEnv === false) {
$configDirEnv = getenv('REDIRECT_SIMPLESAMLPHP_CONFIG_DIR');
}
if ($configDirEnv !== false) {
if (!is_dir($configDirEnv)) {
throw new \InvalidArgumentException(
sprintf(
'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a ' .
'directory. Given: "%s"',
$configDirEnv
)
);
}
$configDir = $configDirEnv;
}
return $configDir;
}
}

View File

@@ -0,0 +1,282 @@
<?php
namespace SimpleSAML\Utils\Config;
/**
* Class with utilities to fetch different configuration objects from metadata configuration arrays.
*
* @package SimpleSAMLphp
* @author Jaime Pérez Crespo, UNINETT AS <jaime.perez@uninett.no>
*/
class Metadata
{
/**
* The string that identities Entity Categories.
*
* @var string
*/
public static $ENTITY_CATEGORY = 'http://macedir.org/entity-category';
/**
* The string the identifies the REFEDS "Hide From Discovery" Entity Category.
*
* @var string
*/
public static $HIDE_FROM_DISCOVERY = 'http://refeds.org/category/hide-from-discovery';
/**
* Valid options for the ContactPerson element
*
* The 'attributes' option isn't defined in section 2.3.2.2 of the OASIS document, but
* it is required to allow additons to the main contact person element for trust
* frameworks.
*
* @var array The valid configuration options for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_OPTIONS = array(
'contactType',
'emailAddress',
'givenName',
'surName',
'telephoneNumber',
'company',
'attributes',
);
/**
* @var array The valid types of contact for a contact configuration array.
* @see "Metadata for the OASIS Security Assertion Markup Language (SAML) V2.0", section 2.3.2.2.
*/
public static $VALID_CONTACT_TYPES = array(
'technical',
'support',
'administrative',
'billing',
'other',
);
/**
* Parse and sanitize a contact from an array.
*
* Accepts an array with the following elements:
* - contactType The type of the contact (as string). Mandatory.
* - emailAddress Email address (as string), or array of email addresses. Optional.
* - telephoneNumber Telephone number of contact (as string), or array of telephone numbers. Optional.
* - name Full name of contact, either as <GivenName> <SurName>, or as <SurName>, <GivenName>. Optional.
* - surName Surname of contact (as string). Optional.
* - givenName Given name of contact (as string). Optional.
* - company Company name of contact (as string). Optional.
*
* The following values are allowed as "contactType":
* - technical
* - support
* - administrative
* - billing
* - other
*
* If given a "name" it will try to decompose it into its given name and surname, only if neither givenName nor
* surName are present. It works as follows:
* - "surname1 surname2, given_name1 given_name2"
* givenName: "given_name1 given_name2"
* surname: "surname1 surname2"
* - "given_name surname"
* givenName: "given_name"
* surname: "surname"
*
* otherwise it will just return the name as "givenName" in the resulting array.
*
* @param array $contact The contact to parse and sanitize.
*
* @return array An array holding valid contact configuration options. If a key 'name' was part of the input array,
* it will try to decompose the name into its parts, and place the parts into givenName and surName, if those are
* missing.
* @throws \InvalidArgumentException If $contact is neither an array nor null, or the contact does not conform to
* valid configuration rules for contacts.
*/
public static function getContact($contact)
{
if (!(is_array($contact) || is_null($contact))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check the type
if (!isset($contact['contactType']) || !in_array($contact['contactType'], self::$VALID_CONTACT_TYPES, true)) {
$types = join(', ', array_map(
function ($t) {
return '"'.$t.'"';
},
self::$VALID_CONTACT_TYPES
));
throw new \InvalidArgumentException('"contactType" is mandatory and must be one of '.$types.".");
}
// check attributes is an associative array
if (isset($contact['attributes'])) {
if (empty($contact['attributes'])
|| !is_array($contact['attributes'])
|| count(array_filter(array_keys($contact['attributes']), 'is_string')) === 0
) {
throw new \InvalidArgumentException('"attributes" must be an array and cannot be empty.');
}
}
// try to fill in givenName and surName from name
if (isset($contact['name']) && !isset($contact['givenName']) && !isset($contact['surName'])) {
// first check if it's comma separated
$names = explode(',', $contact['name'], 2);
if (count($names) === 2) {
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// check if it's in "given name surname" format
$names = explode(' ', preg_replace('/\s+/', ' ', trim($contact['name'])));
if (count($names) === 2) {
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($names[0]));
$contact['surName'] = preg_replace('/\s+/', ' ', trim($names[1]));
} else {
// nothing works, return it as given name
$contact['givenName'] = preg_replace('/\s+/', ' ', trim($contact['name']));
}
}
}
// check givenName
if (isset($contact['givenName']) && (
empty($contact['givenName']) || !is_string($contact['givenName'])
)
) {
throw new \InvalidArgumentException('"givenName" must be a string and cannot be empty.');
}
// check surName
if (isset($contact['surName']) && (
empty($contact['surName']) || !is_string($contact['surName'])
)
) {
throw new \InvalidArgumentException('"surName" must be a string and cannot be empty.');
}
// check company
if (isset($contact['company']) && (
empty($contact['company']) || !is_string($contact['company'])
)
) {
throw new \InvalidArgumentException('"company" must be a string and cannot be empty.');
}
// check emailAddress
if (isset($contact['emailAddress'])) {
if (empty($contact['emailAddress']) ||
!(is_string($contact['emailAddress']) || is_array($contact['emailAddress']))
) {
throw new \InvalidArgumentException('"emailAddress" must be a string or an array and cannot be empty.');
}
if (is_array($contact['emailAddress'])) {
foreach ($contact['emailAddress'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Email addresses must be a string and cannot be empty.');
}
}
}
}
// check telephoneNumber
if (isset($contact['telephoneNumber'])) {
if (empty($contact['telephoneNumber']) ||
!(is_string($contact['telephoneNumber']) || is_array($contact['telephoneNumber']))
) {
throw new \InvalidArgumentException(
'"telephoneNumber" must be a string or an array and cannot be empty.'
);
}
if (is_array($contact['telephoneNumber'])) {
foreach ($contact['telephoneNumber'] as $address) {
if (!is_string($address) || empty($address)) {
throw new \InvalidArgumentException('Telephone numbers must be a string and cannot be empty.');
}
}
}
}
// make sure only valid options are outputted
return array_intersect_key($contact, array_flip(self::$VALID_CONTACT_OPTIONS));
}
/**
* Find the default endpoint in an endpoint array.
*
* @param array $endpoints An array with endpoints.
* @param array $bindings An array with acceptable bindings. Can be null if any binding is allowed.
*
* @return array|NULL The default endpoint, or null if no acceptable endpoints are used.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDefaultEndpoint(array $endpoints, array $bindings = null)
{
$firstNotFalse = null;
$firstAllowed = null;
// look through the endpoint list for acceptable endpoints
foreach ($endpoints as $ep) {
if ($bindings !== null && !in_array($ep['Binding'], $bindings, true)) {
// unsupported binding, skip it
continue;
}
if (isset($ep['isDefault'])) {
if ($ep['isDefault'] === true) {
// this is the first endpoint with isDefault set to true
return $ep;
}
// isDefault is set to false, but the endpoint is still usable as a last resort
if ($firstAllowed === null) {
// this is the first endpoint that we can use
$firstAllowed = $ep;
}
} else {
if ($firstNotFalse === null) {
// this is the first endpoint without isDefault set
$firstNotFalse = $ep;
}
}
}
if ($firstNotFalse !== null) {
// we have an endpoint without isDefault set to false
return $firstNotFalse;
}
/* $firstAllowed either contains the first endpoint we can use, or it contains null if we cannot use any of the
* endpoints. Either way we return its value.
*/
return $firstAllowed;
}
/**
* Determine if an entity should be hidden in the discovery service.
*
* This method searches for the "Hide From Discovery" REFEDS Entity Category, and tells if the entity should be
* hidden or not depending on it.
*
* @see https://refeds.org/category/hide-from-discovery
*
* @param array $metadata An associative array with the metadata representing an entity.
*
* @return boolean True if the entity should be hidden, false otherwise.
*/
public static function isHiddenFromDiscovery(array $metadata)
{
\SimpleSAML\Logger::maskErrors(E_ALL);
$hidden = in_array(self::$HIDE_FROM_DISCOVERY, $metadata['EntityAttributes'][self::$ENTITY_CATEGORY], true);
\SimpleSAML\Logger::popErrorMask();
return $hidden === true;
}
}

471
lib/SimpleSAML/Utils/Crypto.php Executable file
View File

@@ -0,0 +1,471 @@
<?php
namespace SimpleSAML\Utils;
/**
* A class for cryptography-related functions.
*
* @package SimpleSAMLphp
*/
class Crypto
{
/**
* Decrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
* @param string $secret The secret to use to decrypt the data.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesDecrypt()
*/
private static function _aesDecrypt($ciphertext, $secret)
{
if (!is_string($ciphertext)) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
/** @var int $len */
$len = mb_strlen($ciphertext, '8bit');
if ($len < 48) {
throw new \InvalidArgumentException(
'Input parameter "$ciphertext" must be a string with more than 48 characters.'
);
}
if (!function_exists("openssl_decrypt")) {
throw new \SimpleSAML_Error_Exception("The openssl PHP module is not loaded.");
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
$hmac = mb_substr($ciphertext, 0, 32, '8bit');
$iv = mb_substr($ciphertext, 32, 16, '8bit');
$msg = mb_substr($ciphertext, 48, $len - 48, '8bit');
// authenticate the ciphertext
if (self::secureCompare(hash_hmac('sha256', $iv.$msg, substr($key, 64, 64), true), $hmac)) {
$plaintext = openssl_decrypt(
$msg,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($plaintext !== false) {
return $plaintext;
}
}
throw new \SimpleSAML_Error_Exception("Failed to decrypt ciphertext.");
}
/**
* Decrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $ciphertext The HMAC of the encrypted data, the IV used and the encrypted data, concatenated.
*
* @return string The decrypted data.
* @throws \InvalidArgumentException If $ciphertext is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesDecrypt($ciphertext)
{
return self::_aesDecrypt($ciphertext, Config::getSecretSalt());
}
/**
* Encrypt data using AES-256-CBC and the key provided as a parameter.
*
* @param string $data The data to encrypt.
* @param string $secret The secret to use to encrypt the data.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @see \SimpleSAML\Utils\Crypto::aesEncrypt()
*/
private static function _aesEncrypt($data, $secret)
{
if (!is_string($data)) {
throw new \InvalidArgumentException('Input parameter "$data" must be a string.');
}
if (!function_exists("openssl_encrypt")) {
throw new \SimpleSAML_Error_Exception('The openssl PHP module is not loaded.');
}
// derive encryption and authentication keys from the secret
$key = openssl_digest($secret, 'sha512');
// generate a random IV
$iv = openssl_random_pseudo_bytes(16);
// encrypt the message
/** @var string|false $ciphertext */
$ciphertext = openssl_encrypt(
$data,
'AES-256-CBC',
substr($key, 0, 64),
defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : 1,
$iv
);
if ($ciphertext === false) {
throw new \SimpleSAML_Error_Exception("Failed to encrypt plaintext.");
}
// return the ciphertext with proper authentication
return hash_hmac('sha256', $iv.$ciphertext, substr($key, 64, 64), true).$iv.$ciphertext;
}
/**
* Encrypt data using AES-256-CBC and the system-wide secret salt as key.
*
* @param string $data The data to encrypt.
*
* @return string An HMAC of the encrypted data, the IV and the encrypted data, concatenated.
* @throws \InvalidArgumentException If $data is not a string.
* @throws \SimpleSAML_Error_Exception If the openssl module is not loaded.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function aesEncrypt($data)
{
return self::_aesEncrypt($data, Config::getSecretSalt());
}
/**
* Convert data from DER to PEM encoding.
*
* @param string $der Data encoded in DER format.
* @param string $type The type of data we are encoding, as expressed by the PEM header. Defaults to "CERTIFICATE".
* @return string The same data encoded in PEM format.
* @see RFC7648 for known types and PEM format specifics.
*/
public static function der2pem($der, $type = 'CERTIFICATE')
{
return "-----BEGIN ".$type."-----\n".
chunk_split(base64_encode($der), 64, "\n").
"-----END ".$type."-----\n";
}
/**
* Load a private key from metadata.
*
* This function loads a private key from a metadata array. It looks for the following elements:
* - 'privatekey': Name of a private key file in the cert-directory.
* - 'privatekey_pass': Password for the private key.
*
* It returns and array with the following elements:
* - 'PEM': Data for the private key, in PEM-format.
* - 'password': Password for the private key.
*
* @param \SimpleSAML_Configuration $metadata The metadata array the private key should be loaded from.
* @param bool $required Whether the private key is required. If this is true, a
* missing key will cause an exception. Defaults to false.
* @param string $prefix The prefix which should be used when reading from the metadata
* array. Defaults to ''.
* @param bool $full_path Whether the filename found in the configuration contains the
* full path to the private key or not. Default to false.
*
* @return array|NULL Extracted private key, or NULL if no private key is present.
* @throws \InvalidArgumentException If $required is not boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function loadPrivateKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '', $full_path = false)
{
if (!is_bool($required) || !is_string($prefix) || !is_bool($full_path)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$file = $metadata->getString($prefix.'privatekey', null);
if ($file === null) {
// no private key found
if ($required) {
throw new \SimpleSAML_Error_Exception('No private key found in metadata.');
} else {
return null;
}
}
if (!$full_path) {
$file = Config::getCertPath($file);
}
$data = @file_get_contents($file);
if ($data === false) {
throw new \SimpleSAML_Error_Exception('Unable to load private key from file "'.$file.'"');
}
$ret = array(
'PEM' => $data,
);
if ($metadata->hasValue($prefix.'privatekey_pass')) {
$ret['password'] = $metadata->getString($prefix.'privatekey_pass');
}
return $ret;
}
/**
* Get public key or certificate from metadata.
*
* This function implements a function to retrieve the public key or certificate from a metadata array.
*
* It will search for the following elements in the metadata:
* - 'certData': The certificate as a base64-encoded string.
* - 'certificate': A file with a certificate or public key in PEM-format.
* - 'certFingerprint': The fingerprint of the certificate. Can be a single fingerprint, or an array of multiple
* valid fingerprints. (deprecated)
*
* This function will return an array with these elements:
* - 'PEM': The public key/certificate in PEM-encoding.
* - 'certData': The certificate data, base64 encoded, on a single line. (Only present if this is a certificate.)
* - 'certFingerprint': Array of valid certificate fingerprints. (Deprecated. Only present if this is a
* certificate.)
*
* @param \SimpleSAML_Configuration $metadata The metadata.
* @param bool $required Whether the private key is required. If this is TRUE, a missing key
* will cause an exception. Default is FALSE.
* @param string $prefix The prefix which should be used when reading from the metadata array.
* Defaults to ''.
*
* @return array|NULL Public key or certificate data, or NULL if no public key or certificate was found.
* @throws \InvalidArgumentException If $metadata is not an instance of \SimpleSAML_Configuration, $required is not
* boolean or $prefix is not a string.
* @throws \SimpleSAML_Error_Exception If no private key is found in the metadata, or it was not possible to load
* it.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Lasse Birnbaum Jensen
*/
public static function loadPublicKey(\SimpleSAML_Configuration $metadata, $required = false, $prefix = '')
{
if (!is_bool($required) || !is_string($prefix)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$keys = $metadata->getPublicKeys(null, false, $prefix);
if (!empty($keys)) {
foreach ($keys as $key) {
if ($key['type'] !== 'X509Certificate') {
continue;
}
if ($key['signing'] !== true) {
continue;
}
$certData = $key['X509Certificate'];
$pem = "-----BEGIN CERTIFICATE-----\n".
chunk_split($certData, 64).
"-----END CERTIFICATE-----\n";
$certFingerprint = strtolower(sha1(base64_decode($certData)));
return array(
'certData' => $certData,
'PEM' => $pem,
'certFingerprint' => array($certFingerprint),
);
}
// no valid key found
} elseif ($metadata->hasValue($prefix.'certFingerprint')) {
// we only have a fingerprint available
$fps = $metadata->getArrayizeString($prefix.'certFingerprint');
// normalize fingerprint(s) - lowercase and no colons
foreach ($fps as &$fp) {
assert(is_string($fp));
$fp = strtolower(str_replace(':', '', $fp));
}
/*
* We can't build a full certificate from a fingerprint, and may as well return an array with only the
* fingerprint(s) immediately.
*/
return array('certFingerprint' => $fps);
}
// no public key/certificate available
if ($required) {
throw new \SimpleSAML_Error_Exception('No public key / certificate found in metadata.');
} else {
return null;
}
}
/**
* Convert from PEM to DER encoding.
*
* @param string $pem Data encoded in PEM format.
* @return string The same data encoded in DER format.
* @throws \InvalidArgumentException If $pem is not encoded in PEM format.
* @see RFC7648 for PEM format specifics.
*/
public static function pem2der($pem)
{
$pem = trim($pem);
$begin = "-----BEGIN ";
$end = "-----END ";
$lines = explode("\n", $pem);
$last = count($lines) - 1;
if (strpos($lines[0], $begin) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[0]);
if (strpos($lines[$last], $end) !== 0) {
throw new \InvalidArgumentException("pem2der: input is not encoded in PEM format.");
}
unset($lines[$last]);
return base64_decode(implode($lines));
}
/**
* This function hashes a password with a given algorithm.
*
* @param string $password The password to hash.
* @param string $algorithm The hashing algorithm, uppercase, optionally prepended with 'S' (salted). See
* hash_algos() for a complete list of hashing algorithms.
* @param string $salt An optional salt to use.
*
* @return string The hashed password.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @see hash_algos()
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function pwHash($password, $algorithm, $salt = null)
{
if (!is_string($algorithm) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// hash w/o salt
if (in_array(strtolower($algorithm), hash_algos(), true)) {
$alg_str = '{'.str_replace('SHA1', 'SHA', $algorithm).'}'; // LDAP compatibility
$hash = hash(strtolower($algorithm), $password, true);
return $alg_str.base64_encode($hash);
}
// hash w/ salt
if ($salt === null) { // no salt provided, generate one
// default 8 byte salt, but 4 byte for LDAP SHA1 hashes
$bytes = ($algorithm == 'SSHA1') ? 4 : 8;
$salt = openssl_random_pseudo_bytes($bytes);
}
if ($algorithm[0] == 'S' && in_array(substr(strtolower($algorithm), 1), hash_algos(), true)) {
$alg = substr(strtolower($algorithm), 1); // 'sha256' etc
$alg_str = '{'.str_replace('SSHA1', 'SSHA', $algorithm).'}'; // LDAP compatibility
$hash = hash($alg, $password.$salt, true);
return $alg_str.base64_encode($hash.$salt);
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($algorithm).'\' is not supported');
}
/**
* Compare two strings securely.
*
* This method checks if two strings are equal in constant time, avoiding timing attacks. Use it every time we need
* to compare a string with a secret that shouldn't be leaked, i.e. when verifying passwords, one-time codes, etc.
*
* @param string $known A known string.
* @param string $user A user-provided string to compare with the known string.
*
* @return bool True if both strings are equal, false otherwise.
*/
public static function secureCompare($known, $user)
{
if (function_exists('hash_equals')) {
// use hash_equals() if available (PHP >= 5.6)
return hash_equals($known, $user);
}
// compare manually in constant time
$len = mb_strlen($known, '8bit'); // see mbstring.func_overload
if ($len !== mb_strlen($user, '8bit')) {
return false; // length differs
}
$diff = 0;
for ($i = 0; $i < $len; $i++) {
$diff |= ord($known[$i]) ^ ord($user[$i]);
}
// if all the bytes in $a and $b are identical, $diff should be equal to 0
return $diff === 0;
}
/**
* This function checks if a password is valid
*
* @param string $hash The password as it appears in password file, optionally prepended with algorithm.
* @param string $password The password to check in clear.
*
* @return boolean True if the hash corresponds with the given password, false otherwise.
* @throws \InvalidArgumentException If the input parameters are not strings.
* @throws \SimpleSAML_Error_Exception If the algorithm specified is not supported.
*
* @author Dyonisius Visser, TERENA <visser@terena.org>
*/
public static function pwValid($hash, $password)
{
if (!is_string($hash) || !is_string($password)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// match algorithm string (e.g. '{SSHA256}', '{MD5}')
if (preg_match('/^{(.*?)}(.*)$/', $hash, $matches)) {
// LDAP compatibility
$alg = preg_replace('/^(S?SHA)$/', '${1}1', $matches[1]);
// hash w/o salt
if (in_array(strtolower($alg), hash_algos(), true)) {
return self::secureCompare($hash, self::pwHash($password, $alg));
}
// hash w/ salt
if ($alg[0] === 'S' && in_array(substr(strtolower($alg), 1), hash_algos(), true)) {
$php_alg = substr(strtolower($alg), 1);
// get hash length of this algorithm to learn how long the salt is
$hash_length = strlen(hash($php_alg, '', true));
$salt = substr(base64_decode($matches[2]), $hash_length);
return self::secureCompare($hash, self::pwHash($password, $alg, $salt));
}
} else {
return $hash === $password;
}
throw new \SimpleSAML_Error_Exception('Hashing algorithm \''.strtolower($alg).'\' is not supported');
}
}

1222
lib/SimpleSAML/Utils/HTTP.php Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
<?php
namespace SimpleSAML\Utils;
/**
* Provides a non-static wrapper for the HTTP utility class.
*
* @package SimpleSAML\Utils
*/
class HttpAdapter
{
/**
* @see HTTP::getServerHTTPS()
*/
public function getServerHTTPS()
{
return HTTP::getServerHTTPS();
}
/**
* @see HTTP::getServerPort()
*/
public function getServerPort()
{
return HTTP::getServerPort();
}
/**
* @see HTTP::addURLParameters()
*/
public function addURLParameters($url, $parameters)
{
return HTTP::addURLParameters($url, $parameters);
}
/**
* @see HTTP::checkSessionCookie()
*/
public function checkSessionCookie($retryURL = null)
{
HTTP::checkSessionCookie($retryURL);
}
/**
* @see HTTP::checkURLAllowed()
*/
public function checkURLAllowed($url, array $trustedSites = null)
{
return HTTP::checkURLAllowed($url, $trustedSites);
}
/**
* @see HTTP::fetch()
*/
public function fetch($url, $context = array(), $getHeaders = false)
{
return HTTP::fetch($url, $context, $getHeaders);
}
/**
* @see HTTP::getAcceptLanguage()
*/
public function getAcceptLanguage()
{
return HTTP::getAcceptLanguage();
}
/**
* @see HTTP::guessBasePath()
*/
public function guessBasePath()
{
return HTTP::guessBasePath();
}
/**
* @see HTTP::getBaseURL()
*/
public function getBaseURL()
{
return HTTP::getBaseURL();
}
/**
* @see HTTP::getFirstPathElement()
*/
public function getFirstPathElement($trailingslash = true)
{
return HTTP::getFirstPathElement($trailingslash);
}
/**
* @see HTTP::getPOSTRedirectURL()
*/
public function getPOSTRedirectURL($destination, $data)
{
return HTTP::getPOSTRedirectURL($destination, $data);
}
/**
* @see HTTP::getSelfHost()
*/
public function getSelfHost()
{
return HTTP::getSelfHost();
}
/**
* @see HTTP::getSelfHostWithNonStandardPort()
*/
public function getSelfHostWithNonStandardPort()
{
return HTTP::getSelfHostWithNonStandardPort();
}
/**
* @see HTTP::getSelfHostWithPath()
*/
public function getSelfHostWithPath()
{
return HTTP::getSelfHostWithPath();
}
/**
* @see HTTP::getSelfURL()
*/
public function getSelfURL()
{
return HTTP::getSelfURL();
}
/**
* @see HTTP::getSelfURLHost()
*/
public function getSelfURLHost()
{
return HTTP::getSelfURLHost();
}
/**
* @see HTTP::getSelfURLNoQuery()
*/
public function getSelfURLNoQuery()
{
return HTTP::getSelfURLNoQuery();
}
/**
* @see HTTP::isHTTPS()
*/
public function isHTTPS()
{
return HTTP::isHTTPS();
}
/**
* @see HTTP::normalizeURL()
*/
public function normalizeURL($url)
{
return HTTP::normalizeURL($url);
}
/**
* @see HTTP::parseQueryString()
*/
public function parseQueryString($query_string)
{
return HTTP::parseQueryString($query_string);
}
/**
* @see HTTP::redirectTrustedURL()
*/
public function redirectTrustedURL($url, $parameters = array())
{
HTTP::redirectTrustedURL($url, $parameters);
}
/**
* @see HTTP::redirectUntrustedURL()
*/
public function redirectUntrustedURL($url, $parameters = array())
{
HTTP::redirectUntrustedURL($url, $parameters);
}
/**
* @see HTTP::resolveURL()
*/
public function resolveURL($url, $base = null)
{
return HTTP::resolveURL($url, $base);
}
/**
* @see HTTP::setCookie()
*/
public function setCookie($name, $value, $params = null, $throw = true)
{
HTTP::setCookie($name, $value, $params, $throw);
}
/**
* @see HTTP::submitPOSTData()
*/
public function submitPOSTData($destination, $data)
{
HTTP::submitPOSTData($destination, $data);
}
}

85
lib/SimpleSAML/Utils/Net.php Executable file
View File

@@ -0,0 +1,85 @@
<?php
namespace SimpleSAML\Utils;
/**
* Net-related utility methods.
*
* @package SimpleSAMLphp
*/
class Net
{
/**
* Check whether an IP address is part of a CIDR.
*
* @param string $cidr The network CIDR address.
* @param string $ip The IP address to check. Optional. Current remote address will be used if none specified. Do
* not rely on default parameter if running behind load balancers.
*
* @return boolean True if the IP address belongs to the specified CIDR, false otherwise.
*
* @author Andreas Åkre Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Brook Schofield, GÉANT
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function ipCIDRcheck($cidr, $ip = null)
{
if ($ip === null) {
$ip = $_SERVER['REMOTE_ADDR'];
}
if (strpos($cidr, '/') === false) {
return false;
}
list ($net, $mask) = explode('/', $cidr);
$mask = intval($mask);
$ip_ip = array();
$ip_net = array();
if (strstr($ip, ':') || strstr($net, ':')) {
// Validate IPv6 with inet_pton, convert to hex with bin2hex
// then store as a long with hexdec
$ip_pack = @inet_pton($ip);
$net_pack = @inet_pton($net);
if ($ip_pack === false || $net_pack === false) {
// not valid IPv6 address (warning silenced)
return false;
}
$ip_ip = str_split(bin2hex($ip_pack), 8);
foreach ($ip_ip as &$value) {
$value = hexdec($value);
}
$ip_net = str_split(bin2hex($net_pack), 8);
foreach ($ip_net as &$value) {
$value = hexdec($value);
}
} else {
$ip_ip[0] = ip2long($ip);
$ip_net[0] = ip2long($net);
}
for ($i = 0; $mask > 0 && $i < sizeof($ip_ip); $i++) {
if ($mask > 32) {
$iteration_mask = 32;
} else {
$iteration_mask = $mask;
}
$mask -= 32;
$ip_mask = ~((1 << (32 - $iteration_mask)) - 1);
$ip_net_mask = $ip_net[$i] & $ip_mask;
$ip_ip_mask = $ip_ip[$i] & $ip_mask;
if ($ip_ip_mask != $ip_net_mask) {
return false;
}
}
return true;
}
}

30
lib/SimpleSAML/Utils/Random.php Executable file
View File

@@ -0,0 +1,30 @@
<?php
namespace SimpleSAML\Utils;
/**
* Utility class for random data generation and manipulation.
*
* @package SimpleSAMLphp
*/
class Random
{
/**
* The fixed length of random identifiers.
*/
const ID_LENGTH = 43;
/**
* Generate a random identifier, ID_LENGTH bytes long.
*
* @return string A ID_LENGTH-bytes long string with a random, hex-encoded string.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function generateID()
{
return '_'.bin2hex(openssl_random_pseudo_bytes((int)((self::ID_LENGTH - 1)/2)));
}
}

239
lib/SimpleSAML/Utils/System.php Executable file
View File

@@ -0,0 +1,239 @@
<?php
namespace SimpleSAML\Utils;
/**
* System-related utility methods.
*
* @package SimpleSAMLphp
*/
class System
{
const WINDOWS = 1;
const LINUX = 2;
const OSX = 3;
const HPUX = 4;
const UNIX = 5;
const BSD = 6;
const IRIX = 7;
const SUNOS = 8;
/**
* This function returns the Operating System we are running on.
*
* @return mixed A predefined constant identifying the OS we are running on. False if we are unable to determine it.
*
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getOS()
{
if (stristr(PHP_OS, 'LINUX')) {
return self::LINUX;
}
if (stristr(PHP_OS, 'DARWIN')) {
return self::OSX;
}
if (stristr(PHP_OS, 'WIN')) {
return self::WINDOWS;
}
if (stristr(PHP_OS, 'BSD')) {
return self::BSD;
}
if (stristr(PHP_OS, 'UNIX')) {
return self::UNIX;
}
if (stristr(PHP_OS, 'HP-UX')) {
return self::HPUX;
}
if (stristr(PHP_OS, 'IRIX')) {
return self::IRIX;
}
if (stristr(PHP_OS, 'SUNOS')) {
return self::SUNOS;
}
return false;
}
/**
* This function retrieves the path to a directory where temporary files can be saved.
*
* @return string Path to a temporary directory, without a trailing directory separator.
* @throws \SimpleSAML_Error_Exception If the temporary directory cannot be created or it exists and does not belong
* to the current user.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function getTempDir()
{
$globalConfig = \SimpleSAML_Configuration::getInstance();
$tempDir = rtrim(
$globalConfig->getString(
'tempdir',
sys_get_temp_dir().DIRECTORY_SEPARATOR.'simplesaml'
),
DIRECTORY_SEPARATOR
);
if (!is_dir($tempDir)) {
if (!mkdir($tempDir, 0700, true)) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error creating temporary directory "'.$tempDir.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
} elseif (function_exists('posix_getuid')) {
// check that the owner of the temp directory is the current user
$stat = lstat($tempDir);
if ($stat['uid'] !== posix_getuid()) {
throw new \SimpleSAML_Error_Exception(
'Temporary directory "'.$tempDir.'" does not belong to the current user.'
);
}
}
return $tempDir;
}
/**
* Resolve a (possibly) relative path from the given base path.
*
* A path which starts with a '/' is assumed to be absolute, all others are assumed to be
* relative. The default base path is the root of the SimpleSAMLphp installation.
*
* @param string $path The path we should resolve.
* @param string|null $base The base path, where we should search for $path from. Default value is the root of the
* SimpleSAMLphp installation.
*
* @return string An absolute path referring to $path.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function resolvePath($path, $base = null)
{
if ($base === null) {
$config = \SimpleSAML_Configuration::getInstance();
$base = $config->getBaseDir();
}
// normalise directory separator
$base = str_replace('\\', '/', $base);
$path = str_replace('\\', '/', $path);
// remove trailing slashes
$base = rtrim($base, '/');
$path = rtrim($path, '/');
// check for absolute path
if (substr($path, 0, 1) === '/') {
// absolute path. */
$ret = '/';
} elseif (static::pathContainsDriveLetter($path)) {
$ret = '';
} else {
// path relative to base
$ret = $base;
}
$path = explode('/', $path);
foreach ($path as $d) {
if ($d === '.') {
continue;
} elseif ($d === '..') {
$ret = dirname($ret);
} else {
if ($ret && substr($ret, -1) !== '/') {
$ret .= '/';
}
$ret .= $d;
}
}
return $ret;
}
/**
* Atomically write a file.
*
* This is a helper function for writing data atomically to a file. It does this by writing the file data to a
* temporary file, then renaming it to the required file name.
*
* @param string $filename The path to the file we want to write to.
* @param string $data The data we should write to the file.
* @param int $mode The permissions to apply to the file. Defaults to 0600.
*
* @throws \InvalidArgumentException If any of the input parameters doesn't have the proper types.
* @throws \SimpleSAML_Error_Exception If the file cannot be saved, permissions cannot be changed or it is not
* possible to write to the target file.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Andjelko Horvat
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*
* @return void
*/
public static function writeFile($filename, $data, $mode = 0600)
{
if (!is_string($filename) || !is_string($data) || !is_numeric($mode)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
$tmpFile = self::getTempDir().DIRECTORY_SEPARATOR.rand();
$res = @file_put_contents($tmpFile, $data);
if ($res === false) {
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error saving file "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (self::getOS() !== self::WINDOWS) {
if (!chmod($tmpFile, $mode)) {
unlink($tmpFile);
$error = error_get_last();
//$error = (is_array($error) ? $error['message'] : 'no error available');
throw new \SimpleSAML_Error_Exception(
'Error changing file mode of "'.$tmpFile.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
}
if (!rename($tmpFile, $filename)) {
unlink($tmpFile);
$error = error_get_last();
throw new \SimpleSAML_Error_Exception(
'Error moving "'.$tmpFile.'" to "'.$filename.'": '.
(is_array($error) ? $error['message'] : 'no error available')
);
}
if (function_exists('opcache_invalidate')) {
opcache_invalidate($filename);
}
}
/**
* Check if the supplied path contains a Windows-style drive letter.
*
* @param string $path
*
* @return bool
*/
private static function pathContainsDriveLetter($path)
{
$letterAsciiValue = ord(strtoupper(substr($path, 0, 1)));
return substr($path, 1, 1) === ':'
&& $letterAsciiValue >= 65 && $letterAsciiValue <= 90;
}
}

167
lib/SimpleSAML/Utils/Time.php Executable file
View File

@@ -0,0 +1,167 @@
<?php
/**
* Time-related utility methods.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
class Time
{
/**
* Whether the timezone has been initialized or not.
*
* @var bool
*/
private static $tz_initialized = false;
/**
* This function generates a timestamp on the form used by the SAML protocols.
*
* @param int $instant The time the timestamp should represent. Defaults to current time.
*
* @return string The timestamp.
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function generateTimestamp($instant = null)
{
if ($instant === null) {
$instant = time();
}
return gmdate('Y-m-d\TH:i:s\Z', $instant);
}
/**
* Initialize the timezone.
*
* This function should be called before any calls to date().
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*
* @throws \SimpleSAML_Error_Exception If the timezone set in the configuration is invalid.
*
* @return void
*/
public static function initTimezone()
{
if (self::$tz_initialized) {
return;
}
$globalConfig = \SimpleSAML_Configuration::getInstance();
$timezone = $globalConfig->getString('timezone', null);
if ($timezone !== null) {
if (!date_default_timezone_set($timezone)) {
throw new \SimpleSAML_Error_Exception('Invalid timezone set in the "timezone" option in config.php.');
}
self::$tz_initialized = true;
return;
}
// we don't have a timezone configured
Logger::maskErrors(E_ALL);
$serverTimezone = date_default_timezone_get();
Logger::popErrorMask();
// set the timezone to the default
date_default_timezone_set($serverTimezone);
self::$tz_initialized = true;
}
/**
* Interpret a ISO8601 duration value relative to a given timestamp. Please note no fractions are allowed, neither
* durations specified in the formats PYYYYMMDDThhmmss nor P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss].
*
* @param string $duration The duration, as a string.
* @param int $timestamp The unix timestamp we should apply the duration to. Optional, default to the current
* time.
*
* @return int The new timestamp, after the duration is applied.
* @throws \InvalidArgumentException If $duration is not a valid ISO 8601 duration or if the input parameters do
* not have the right data types.
*/
public static function parseDuration($duration, $timestamp = null)
{
if (!(is_string($duration) && (is_int($timestamp) || is_null($timestamp)))) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// parse the duration. We use a very strict pattern
$durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)'.
'(?:[.,]\d+)?S)?)?)|(?:(\\d+)W))$#D';
if (!preg_match($durationRegEx, $duration, $matches)) {
throw new \InvalidArgumentException('Invalid ISO 8601 duration: '.$duration);
}
$durYears = (empty($matches[2]) ? 0 : (int) $matches[2]);
$durMonths = (empty($matches[3]) ? 0 : (int) $matches[3]);
$durDays = (empty($matches[4]) ? 0 : (int) $matches[4]);
$durHours = (empty($matches[5]) ? 0 : (int) $matches[5]);
$durMinutes = (empty($matches[6]) ? 0 : (int) $matches[6]);
$durSeconds = (empty($matches[7]) ? 0 : (int) $matches[7]);
$durWeeks = (empty($matches[8]) ? 0 : (int) $matches[8]);
if (!empty($matches[1])) {
// negative
$durYears = -$durYears;
$durMonths = -$durMonths;
$durDays = -$durDays;
$durHours = -$durHours;
$durMinutes = -$durMinutes;
$durSeconds = -$durSeconds;
$durWeeks = -$durWeeks;
}
if ($timestamp === null) {
$timestamp = time();
}
if ($durYears !== 0 || $durMonths !== 0) {
/* Special handling of months and years, since they aren't a specific interval, but
* instead depend on the current time.
*/
/* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the
* gmtime function. Instead we use the gmdate function, and split the result.
*/
$yearmonth = explode(':', gmdate('Y:n', $timestamp));
$year = (int) ($yearmonth[0]);
$month = (int) ($yearmonth[1]);
// remove the year and month from the timestamp
$timestamp -= gmmktime(0, 0, 0, $month, 1, $year);
// add years and months, and normalize the numbers afterwards
$year += $durYears;
$month += $durMonths;
while ($month > 12) {
$year += 1;
$month -= 12;
}
while ($month < 1) {
$year -= 1;
$month += 12;
}
// add year and month back into timestamp
$timestamp += gmmktime(0, 0, 0, $month, 1, $year);
}
// add the other elements
$timestamp += $durWeeks * 7 * 24 * 60 * 60;
$timestamp += $durDays * 24 * 60 * 60;
$timestamp += $durHours * 60 * 60;
$timestamp += $durMinutes * 60;
$timestamp += $durSeconds;
return $timestamp;
}
}

453
lib/SimpleSAML/Utils/XML.php Executable file
View File

@@ -0,0 +1,453 @@
<?php
/**
* Utility class for XML and DOM manipulation.
*
* @package SimpleSAMLphp
*/
namespace SimpleSAML\Utils;
use SimpleSAML\Logger;
use SimpleSAML\XML\Errors;
class XML
{
/**
* This function performs some sanity checks on XML documents, and optionally validates them against their schema
* if the 'validatexml' debugging option is enabled. A warning will be printed to the log if validation fails.
*
* @param string $message The SAML document we want to check.
* @param string $type The type of document. Can be one of:
* - 'saml20'
* - 'saml11'
* - 'saml-meta'
*
* @throws \InvalidArgumentException If $message is not a string or $type is not a string containing one of the
* values allowed.
* @throws \SimpleSAML_Error_Exception If $message contains a doctype declaration.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
* @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no>
*/
public static function checkSAMLMessage($message, $type)
{
$allowed_types = array('saml20', 'saml11', 'saml-meta');
if (!(is_string($message) && in_array($type, $allowed_types, true))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// a SAML message should not contain a doctype-declaration
if (strpos($message, '<!DOCTYPE') !== false) {
throw new \SimpleSAML_Error_Exception('XML contained a doctype declaration.');
}
// see if debugging is enabled for XML validation
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('validatexml' => false));
$enabled = \SimpleSAML_Configuration::getInstance()->getBoolean('debug.validatexml', false);
if (!(in_array('validatexml', $debug, true) // implicitly enabled
|| (array_key_exists('validatexml', $debug) && $debug['validatexml'] === true) // explicitly enabled
// TODO: deprecate this option and remove it in 2.0
|| $enabled // old 'debug.validatexml' configuration option
)) {
// XML validation is disabled
return;
}
$result = true;
switch ($type) {
case 'saml11':
$result = self::isValid($message, 'oasis-sstc-saml-schema-protocol-1.1.xsd');
break;
case 'saml20':
$result = self::isValid($message, 'saml-schema-protocol-2.0.xsd');
break;
case 'saml-meta':
$result = self::isValid($message, 'saml-schema-metadata-2.0.xsd');
}
if ($result !== true) {
Logger::warning($result);
}
}
/**
* Helper function to log SAML messages that we send or receive.
*
* @param string|\DOMElement $message The message, as an string containing the XML or an XML element.
* @param string $type Whether this message is sent or received, encrypted or decrypted. The following
* values are supported:
* - 'in': for messages received.
* - 'out': for outgoing messages.
* - 'decrypt': for decrypted messages.
* - 'encrypt': for encrypted messages.
*
* @throws \InvalidArgumentException If $type is not a string or $message is neither a string nor a \DOMElement.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function debugSAMLMessage($message, $type)
{
if (!(is_string($type) && (is_string($message) || $message instanceof \DOMElement))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
// see if debugging is enabled for SAML messages
$debug = \SimpleSAML_Configuration::getInstance()->getArrayize('debug', array('saml' => false));
if (!(in_array('saml', $debug, true) // implicitly enabled
|| (array_key_exists('saml', $debug) && $debug['saml'] === true) // explicitly enabled
// TODO: deprecate the old style and remove it in 2.0
|| (array_key_exists(0, $debug) && $debug[0] === true) // old style 'debug'
)) {
// debugging messages is disabled
return;
}
if ($message instanceof \DOMElement) {
$message = $message->ownerDocument->saveXML($message);
}
switch ($type) {
case 'in':
Logger::debug('Received message:');
break;
case 'out':
Logger::debug('Sending message:');
break;
case 'decrypt':
Logger::debug('Decrypted message:');
break;
case 'encrypt':
Logger::debug('Encrypted message:');
break;
default:
assert(false);
}
$str = self::formatXMLString($message);
foreach (explode("\n", $str) as $line) {
Logger::debug($line);
}
}
/**
* Format a DOM element.
*
* This function takes in a DOM element, and inserts whitespace to make it more readable. Note that whitespace
* added previously will be removed.
*
* @param \DOMNode $root The root element which should be formatted.
* @param string $indentBase The indentation this element should be assumed to have. Defaults to an empty
* string.
*
* @throws \InvalidArgumentException If $root is not a DOMElement or $indentBase is not a string.
*
* @return void
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatDOMElement(\DOMNode $root, $indentBase = '')
{
if (!is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
// check what this element contains
$fullText = ''; // all text in this element
$textNodes = array(); // text nodes which should be deleted
$childNodes = array(); // other child nodes
for ($i = 0; $i < $root->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $root->childNodes->item($i);
if ($child instanceof \DOMText) {
$textNodes[] = $child;
$fullText .= $child->wholeText;
} elseif ($child instanceof \DOMComment || $child instanceof \DOMElement) {
$childNodes[] = $child;
} else {
// unknown node type. We don't know how to format this
return;
}
}
$fullText = trim($fullText);
if (strlen($fullText) > 0) {
// we contain textelf
$hasText = true;
} else {
$hasText = false;
}
$hasChildNode = (count($childNodes) > 0);
if ($hasText && $hasChildNode) {
// element contains both text and child nodes - we don't know how to format this one
return;
}
// remove text nodes
foreach ($textNodes as $node) {
$root->removeChild($node);
}
if ($hasText) {
// only text - add a single text node to the element with the full text
$root->appendChild(new \DOMText($fullText));
return;
}
if (!$hasChildNode) {
// empty node. Nothing to do
return;
}
/* Element contains only child nodes - add indentation before each one, and
* format child elements.
*/
$childIndentation = $indentBase.' ';
foreach ($childNodes as $node) {
// add indentation before node
$root->insertBefore(new \DOMText("\n".$childIndentation), $node);
// format child elements
if ($node instanceof \DOMElement) {
self::formatDOMElement($node, $childIndentation);
}
}
// add indentation before closing tag
$root->appendChild(new \DOMText("\n".$indentBase));
}
/**
* Format an XML string.
*
* This function formats an XML string using the formatDOMElement() function.
*
* @param string $xml An XML string which should be formatted.
* @param string $indentBase Optional indentation which should be applied to all the output. Optional, defaults
* to ''.
*
* @return string The formatted string.
* @throws \InvalidArgumentException If the parameters are not strings.
* @throws \DOMException If the input does not parse correctly as an XML string.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function formatXMLString($xml, $indentBase = '')
{
if (!is_string($xml) || !is_string($indentBase)) {
throw new \InvalidArgumentException('Invalid input parameters');
}
try {
$doc = \SAML2\DOMDocumentFactory::fromString($xml);
} catch (\Exception $e) {
throw new \DOMException('Error parsing XML string.');
}
$root = $doc->firstChild;
self::formatDOMElement($root, $indentBase);
return $doc->saveXML($root);
}
/**
* This function finds direct descendants of a DOM element with the specified
* localName and namespace. They are returned in an array.
*
* This function accepts the same shortcuts for namespaces as the isDOMNodeOfType function.
*
* @param \DOMNode $element The element we should look in.
* @param string $localName The name the element should have.
* @param string $namespaceURI The namespace the element should have.
*
* @return array Array with the matching elements in the order they are found. An empty array is
* returned if no elements match.
* @throws \InvalidArgumentException If $element is not an instance of DOMElement, $localName is not a string or
* $namespaceURI is not a string.
*/
public static function getDOMChildren(\DOMNode $element, $localName, $namespaceURI)
{
if (!is_string($localName) || !is_string($namespaceURI)) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
$ret = array();
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
// skip text nodes and comment elements
if ($child instanceof \DOMText || $child instanceof \DOMComment) {
continue;
}
if (self::isDOMNodeOfType($child, $localName, $namespaceURI) === true) {
$ret[] = $child;
}
}
return $ret;
}
/**
* This function extracts the text from DOMElements which should contain only text content.
*
* @param \DOMElement $element The element we should extract text from.
*
* @return string The text content of the element.
* @throws \SimpleSAML_Error_Exception If the element contains a non-text child node.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function getDOMText(\DOMElement $element)
{
$txt = '';
for ($i = 0; $i < $element->childNodes->length; $i++) {
/** @var \DOMElement $child */
$child = $element->childNodes->item($i);
if (!($child instanceof \DOMText)) {
throw new \SimpleSAML_Error_Exception($element->localName.' contained a non-text child node.');
}
$txt .= $child->wholeText;
}
$txt = trim($txt);
return $txt;
}
/**
* This function checks if the DOMElement has the correct localName and namespaceURI.
*
* We also define the following shortcuts for namespaces:
* - '@ds': 'http://www.w3.org/2000/09/xmldsig#'
* - '@md': 'urn:oasis:names:tc:SAML:2.0:metadata'
* - '@saml1': 'urn:oasis:names:tc:SAML:1.0:assertion'
* - '@saml1md': 'urn:oasis:names:tc:SAML:profiles:v1metadata'
* - '@saml1p': 'urn:oasis:names:tc:SAML:1.0:protocol'
* - '@saml2': 'urn:oasis:names:tc:SAML:2.0:assertion'
* - '@saml2p': 'urn:oasis:names:tc:SAML:2.0:protocol'
*
* @param \DOMNode $element The element we should check.
* @param string $name The local name the element should have.
* @param string $nsURI The namespaceURI the element should have.
*
* @return boolean True if both namespace and local name matches, false otherwise.
* @throws \InvalidArgumentException If the namespace shortcut is unknown.
*
* @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no>
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isDOMNodeOfType(\DOMNode $element, $name, $nsURI)
{
if (!is_string($name) || !is_string($nsURI) || strlen($nsURI) === 0) {
// most likely a comment-node
return false;
}
// check if the namespace is a shortcut, and expand it if it is
if ($nsURI[0] === '@') {
// the defined shortcuts
$shortcuts = array(
'@ds' => 'http://www.w3.org/2000/09/xmldsig#',
'@md' => 'urn:oasis:names:tc:SAML:2.0:metadata',
'@saml1' => 'urn:oasis:names:tc:SAML:1.0:assertion',
'@saml1md' => 'urn:oasis:names:tc:SAML:profiles:v1metadata',
'@saml1p' => 'urn:oasis:names:tc:SAML:1.0:protocol',
'@saml2' => 'urn:oasis:names:tc:SAML:2.0:assertion',
'@saml2p' => 'urn:oasis:names:tc:SAML:2.0:protocol',
'@shibmd' => 'urn:mace:shibboleth:metadata:1.0',
);
// check if it is a valid shortcut
if (!array_key_exists($nsURI, $shortcuts)) {
throw new \InvalidArgumentException('Unknown namespace shortcut: '.$nsURI);
}
// expand the shortcut
$nsURI = $shortcuts[$nsURI];
}
if ($element->localName !== $name) {
return false;
}
if ($element->namespaceURI !== $nsURI) {
return false;
}
return true;
}
/**
* This function attempts to validate an XML string against the specified schema. It will parse the string into a
* DOM document and validate this document against the schema.
*
* Note that this function returns values that are evaluated as a logical true, both when validation works and when
* it doesn't. Please use strict comparisons to check the values returned.
*
* @param string|\DOMDocument $xml The XML string or document which should be validated.
* @param string $schema The filename of the schema that should be used to validate the document.
*
* @return boolean|string Returns a string with errors found if validation fails. True if validation passes ok.
* @throws \InvalidArgumentException If $schema is not a string, or $xml is neither a string nor a \DOMDocument.
*
* @author Olav Morken, UNINETT AS <olav.morken@uninett.no>
*/
public static function isValid($xml, $schema)
{
if (!(is_string($schema) && (is_string($xml) || $xml instanceof \DOMDocument))) {
throw new \InvalidArgumentException('Invalid input parameters.');
}
Errors::begin();
if ($xml instanceof \DOMDocument) {
$dom = $xml;
$res = true;
} else {
try {
$dom = \SAML2\DOMDocumentFactory::fromString($xml);
$res = true;
} catch (\Exception $e) {
$res = false;
}
}
if ($res) {
$config = \SimpleSAML_Configuration::getInstance();
/** @var string $schemaPath */
$schemaPath = $config->resolvePath('schemas');
$schemaFile = $schemaPath.'/'.$schema;
$res = $dom->schemaValidate($schemaFile);
if ($res) {
Errors::end();
return true;
}
$errorText = "Schema validation failed on XML string:\n";
} else {
$errorText = "Failed to parse XML string for schema validation:\n";
}
$errors = Errors::end();
$errorText .= Errors::formatErrors($errors);
return $errorText;
}
}

122
lib/SimpleSAML/XHTML/EMail.php Executable file
View File

@@ -0,0 +1,122 @@
<?php
/**
* A minimalistic Emailer class. Creates and sends HTML emails.
*
* @author Andreas kre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_EMail
{
private $to = null;
private $cc = null;
private $body = null;
private $from = null;
private $replyto = null;
private $subject = null;
private $headers = array();
/**
* Constructor
*/
public function __construct($to, $subject, $from = null, $cc = null, $replyto = null)
{
$this->to = $to;
$this->cc = $cc;
$this->from = $from;
$this->replyto = $replyto;
$this->subject = $subject;
}
/*
* @param string $body
* @return void
*/
public function setBody($body)
{
$this->body = $body;
}
/*
* @param string $body
* @return void
*/
private function getHTML($body)
{
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>SimpleSAMLphp Email report</title>
<style type="text/css">
pre, div.box {
margin: .4em 2em .4em 1em;
padding: 4px;
}
pre {
background: #eee;
border: 1px solid #aaa;
}
</style>
</head>
<body>
<div class="container" style="background: #fafafa; border: 1px solid #eee; margin: 2em; padding: .6em;">
' . $body . '
</div>
</body>
</html>';
}
/*
* @return void
*/
public function send()
{
if ($this->to === null) {
throw new Exception('EMail field [to] is required and not set.');
} elseif ($this->subject === null) {
throw new Exception('EMail field [subject] is required and not set.');
} elseif ($this->body === null) {
throw new Exception('EMail field [body] is required and not set.');
}
$random_hash = bin2hex(openssl_random_pseudo_bytes(16));
if (isset($this->from)) {
$this->headers[]= 'From: ' . $this->from;
}
if (isset($this->replyto)) {
$this->headers[]= 'Reply-To: ' . $this->replyto;
}
$this->headers[] = 'Content-Type: multipart/alternative; boundary="simplesamlphp-' . $random_hash . '"';
$message = '
--simplesamlphp-' . $random_hash . '
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . strip_tags(html_entity_decode($this->body)) . '
--simplesamlphp-' . $random_hash . '
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit
' . $this->getHTML($this->body) . '
--simplesamlphp-' . $random_hash . '--
';
$headers = implode("\n", $this->headers);
$mail_sent = @mail($this->to, $this->subject, $message, $headers);
SimpleSAML\Logger::debug('Email: Sending e-mail to [' . $this->to . '] : ' . ($mail_sent ? 'OK' : 'Failed'));
if (!$mail_sent) {
throw new Exception('Error when sending e-mail');
}
}
}

598
lib/SimpleSAML/XHTML/IdPDisco.php Executable file
View File

@@ -0,0 +1,598 @@
<?php
/**
* This class implements a generic IdP discovery service, for use in various IdP
* discovery service pages. This should reduce code duplication.
*
* Experimental support added for Extended IdP Metadata Discovery Protocol by Andreas 2008-08-28
* More information: https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-idp-discovery.pdf
*
* @author Jaime Pérez <jaime.perez@uninett.no>, UNINETT AS.
* @author Olav Morken, UNINETT AS.
* @author Andreas Åkre Solberg <andreas@uninett.no>, UNINETT AS.
* @package SimpleSAMLphp
*/
class SimpleSAML_XHTML_IdPDisco
{
/**
* An instance of the configuration class.
*
* @var SimpleSAML_Configuration
*/
protected $config;
/**
* The identifier of this discovery service.
*
* @var string
*/
protected $instance;
/**
* An instance of the metadata handler, which will allow us to fetch metadata about IdPs.
*
* @var SimpleSAML_Metadata_MetaDataStorageHandler
*/
protected $metadata;
/**
* The users session.
*
* @var SimpleSAML_Session
*/
protected $session;
/**
* The metadata sets we find allowed entities in, in prioritized order.
*
* @var array
*/
protected $metadataSets;
/**
* The entity id of the SP which accesses this IdP discovery service.
*
* @var string
*/
protected $spEntityId;
/**
* HTTP parameter from the request, indicating whether the discovery service
* can interact with the user or not.
*
* @var boolean
*/
protected $isPassive;
/**
* The SP request to set the IdPentityID...
*
* @var string|null
*/
protected $setIdPentityID = null;
/**
* The name of the query parameter which should contain the users choice of IdP.
* This option default to 'entityID' for Shibboleth compatibility.
*
* @var string
*/
protected $returnIdParam;
/**
* The list of scoped idp's. The intersection between the metadata idpList
* and scopedIDPList (given as a $_GET IDPList[] parameter) is presented to
* the user. If the intersection is empty the metadata idpList is used.
*
* @var array
*/
protected $scopedIDPList = array();
/**
* The URL the user should be redirected to after choosing an IdP.
*
* @var string
*/
protected $returnURL;
/**
* Initializes this discovery service.
*
* The constructor does the parsing of the request. If this is an invalid request, it will throw an exception.
*
* @param array $metadataSets Array with metadata sets we find remote entities in.
* @param string $instance The name of this instance of the discovery service.
*
* @throws Exception If the request is invalid.
*/
public function __construct(array $metadataSets, $instance)
{
assert(is_string($instance));
// initialize standard classes
$this->config = SimpleSAML_Configuration::getInstance();
$this->metadata = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
$this->session = SimpleSAML_Session::getSessionFromRequest();
$this->instance = $instance;
$this->metadataSets = $metadataSets;
$this->log('Accessing discovery service.');
// standard discovery service parameters
if (!array_key_exists('entityID', $_GET)) {
throw new Exception('Missing parameter: entityID');
} else {
$this->spEntityId = $_GET['entityID'];
}
if (!array_key_exists('returnIDParam', $_GET)) {
$this->returnIdParam = 'entityID';
} else {
$this->returnIdParam = $_GET['returnIDParam'];
}
$this->log('returnIdParam initially set to ['.$this->returnIdParam.']');
if (!array_key_exists('return', $_GET)) {
throw new Exception('Missing parameter: return');
} else {
$this->returnURL = \SimpleSAML\Utils\HTTP::checkURLAllowed($_GET['return']);
}
$this->isPassive = false;
if (array_key_exists('isPassive', $_GET)) {
if ($_GET['isPassive'] === 'true') {
$this->isPassive = true;
}
}
$this->log('isPassive initially set to ['.($this->isPassive ? 'TRUE' : 'FALSE').']');
if (array_key_exists('IdPentityID', $_GET)) {
$this->setIdPentityID = $_GET['IdPentityID'];
}
if (array_key_exists('IDPList', $_REQUEST)) {
$this->scopedIDPList = $_REQUEST['IDPList'];
}
}
/**
* Log a message.
*
* This is an helper function for logging messages. It will prefix the messages with our
* discovery service type.
*
* @param string $message The message which should be logged.
*/
protected function log($message)
{
SimpleSAML\Logger::info('idpDisco.'.$this->instance.': '.$message);
}
/**
* Retrieve cookie with the given name.
*
* This function will retrieve a cookie with the given name for the current discovery
* service type.
*
* @param string $name The name of the cookie.
*
* @return string The value of the cookie with the given name, or null if no cookie with that name exists.
*/
protected function getCookie($name)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
if (array_key_exists($prefixedName, $_COOKIE)) {
return $_COOKIE[$prefixedName];
} else {
return null;
}
}
/**
* Save cookie with the given name and value.
*
* This function will save a cookie with the given name and value for the current discovery
* service type.
*
* @param string $name The name of the cookie.
* @param string $value The value of the cookie.
*/
protected function setCookie($name, $value)
{
$prefixedName = 'idpdisco_'.$this->instance.'_'.$name;
$params = array(
// we save the cookies for 90 days
'lifetime' => (60 * 60 * 24 * 90),
// the base path for cookies. This should be the installation directory for SimpleSAMLphp
'path' => $this->config->getBasePath(),
'httponly' => false,
);
\SimpleSAML\Utils\HTTP::setCookie($prefixedName, $value, $params, false);
}
/**
* Validates the given IdP entity id.
*
* Takes a string with the IdP entity id, and returns the entity id if it is valid, or
* null if not.
*
* @param string|null $idp The entity id we want to validate. This can be null, in which case we will return null.
*
* @return string|null The entity id if it is valid, null if not.
*/
protected function validateIdP($idp)
{
if ($idp === null) {
return null;
}
if (!$this->config->getBoolean('idpdisco.validate', true)) {
return $idp;
}
foreach ($this->metadataSets as $metadataSet) {
try {
$this->metadata->getMetaData($idp, $metadataSet);
return $idp;
} catch (Exception $e) {
// continue
}
}
$this->log('Unable to validate IdP entity id ['.$idp.'].');
// the entity id wasn't valid
return null;
}
/**
* Retrieve the users choice of IdP.
*
* This function finds out which IdP the user has manually chosen, if any.
*
* @return string The entity id of the IdP the user has chosen, or null if the user has made no choice.
*/
protected function getSelectedIdP()
{
/* Parameter set from the Extended IdP Metadata Discovery Service Protocol, indicating that the user prefers
* this IdP.
*/
if (!empty($this->setIdPentityID)) {
return $this->validateIdP($this->setIdPentityID);
}
// user has clicked on a link, or selected the IdP from a drop-down list
if (array_key_exists('idpentityid', $_GET)) {
return $this->validateIdP($_GET['idpentityid']);
}
/* Search for the IdP selection from the form used by the links view. This form uses a name which equals
* idp_<entityid>, so we search for that.
*
* Unfortunately, php replaces periods in the name with underscores, and there is no reliable way to get them
* back. Therefore we do some quick and dirty parsing of the query string.
*/
$qstr = $_SERVER['QUERY_STRING'];
$matches = array();
if (preg_match('/(?:^|&)idp_([^=]+)=/', $qstr, $matches)) {
return $this->validateIdP(urldecode($matches[1]));
}
// no IdP chosen
return null;
}
/**
* Retrieve the users saved choice of IdP.
*
* @return string The entity id of the IdP the user has saved, or null if the user hasn't saved any choice.
*/
protected function getSavedIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return null;
}
if ($this->getCookie('remember') === '1') {
$this->log('Return previously saved IdP because of remember cookie set to 1');
return $this->getPreviousIdP();
}
if ($this->isPassive) {
$this->log('Return previously saved IdP because of isPassive');
return $this->getPreviousIdP();
}
return null;
}
/**
* Retrieve the previous IdP the user used.
*
* @return string The entity id of the previous IdP the user used, or null if this is the first time.
*/
protected function getPreviousIdP()
{
return $this->validateIdP($this->getCookie('lastidp'));
}
/**
* Retrieve a recommended IdP based on the IP address of the client.
*
* @return string|null The entity ID of the IdP if one is found, or null if not.
*/
protected function getFromCIDRhint()
{
foreach ($this->metadataSets as $metadataSet) {
$idp = $this->metadata->getPreferredEntityIdFromCIDRhint($metadataSet, $_SERVER['REMOTE_ADDR']);
if (!empty($idp)) {
return $idp;
}
}
return null;
}
/**
* Try to determine which IdP the user should most likely use.
*
* This function will first look at the previous IdP the user has chosen. If the user
* hasn't chosen an IdP before, it will look at the IP address.
*
* @return string The entity id of the IdP the user should most likely use.
*/
protected function getRecommendedIdP()
{
$idp = $this->getPreviousIdP();
if ($idp !== null) {
$this->log('Preferred IdP from previous use ['.$idp.'].');
return $idp;
}
$idp = $this->getFromCIDRhint();
if (!empty($idp)) {
$this->log('Preferred IdP from CIDR hint ['.$idp.'].');
return $idp;
}
return null;
}
/**
* Save the current IdP choice to a cookie.
*
* @param string $idp The entityID of the IdP.
*/
protected function setPreviousIdP($idp)
{
assert(is_string($idp));
$this->log('Choice made ['.$idp.'] Setting cookie.');
$this->setCookie('lastidp', $idp);
}
/**
* Determine whether the choice of IdP should be saved.
*
* @return boolean True if the choice should be saved, false otherwise.
*/
protected function saveIdP()
{
if (!$this->config->getBoolean('idpdisco.enableremember', false)) {
// saving of IdP choices is disabled
return false;
}
if (array_key_exists('remember', $_GET)) {
return true;
}
return false;
}
/**
* Determine which IdP the user should go to, if any.
*
* @return string The entity id of the IdP the user should be sent to, or null if the user should choose.
*/
protected function getTargetIdP()
{
// first, check if the user has chosen an IdP
$idp = $this->getSelectedIdP();
if ($idp !== null) {
// the user selected this IdP. Save the choice in a cookie
$this->setPreviousIdP($idp);
if ($this->saveIdP()) {
$this->setCookie('remember', '1');
} else {
$this->setCookie('remember', '0');
}
return $idp;
}
$this->log('getSelectedIdP() returned null');
// check if the user has saved an choice earlier
$idp = $this->getSavedIdP();
if ($idp !== null) {
$this->log('Using saved choice ['.$idp.'].');
return $idp;
}
// the user has made no choice
return null;
}
/**
* Retrieve the list of IdPs which are stored in the metadata.
*
* @return array An array with entityid => metadata mappings.
*/
protected function getIdPList()
{
$idpList = array();
foreach ($this->metadataSets as $metadataSet) {
$newList = $this->metadata->getList($metadataSet);
/*
* Note that we merge the entities in reverse order. This ensures that it is the entity in the first
* metadata set that "wins" if two metadata sets have the same entity.
*/
$idpList = array_merge($newList, $idpList);
}
return $idpList;
}
/**
* Return the list of scoped idp
*
* @return array An array of IdP entities
*/
protected function getScopedIDPList()
{
return $this->scopedIDPList;
}
/**
* Filter the list of IdPs.
*
* This method returns the IdPs that comply with the following conditions:
* - The IdP does not have the 'hide.from.discovery' configuration option.
*
* @param array $list An associative array containing metadata for the IdPs to apply the filtering to.
*
* @return array An associative array containing metadata for the IdPs that were not filtered out.
*/
protected function filterList($list)
{
foreach ($list as $entity => $metadata) {
if (array_key_exists('hide.from.discovery', $metadata) && $metadata['hide.from.discovery'] === true) {
unset($list[$entity]);
}
}
return $list;
}
/**
* Check if an IdP is set or if the request is passive, and redirect accordingly.
*
* @return void If there is no IdP targeted and this is not a passive request.
*/
protected function start()
{
$idp = $this->getTargetIdp();
if ($idp !== null) {
$extDiscoveryStorage = $this->config->getString('idpdisco.extDiscoveryStorage', null);
if ($extDiscoveryStorage !== null) {
$this->log('Choice made ['.$idp.'] (Forwarding to external discovery storage)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($extDiscoveryStorage, array(
'entityID' => $this->spEntityId,
'IdPentityID' => $idp,
'returnIDParam' => $this->returnIdParam,
'isPassive' => 'true',
'return' => $this->returnURL
));
} else {
$this->log(
'Choice made ['.$idp.'] (Redirecting the user back. returnIDParam='.$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL, array($this->returnIdParam => $idp));
}
}
if ($this->isPassive) {
$this->log('Choice not made. (Redirecting the user back without answer)');
\SimpleSAML\Utils\HTTP::redirectTrustedURL($this->returnURL);
}
}
/**
* Handles a request to this discovery service.
*
* The IdP disco parameters should be set before calling this function.
*/
public function handleRequest()
{
$this->start();
// no choice made. Show discovery service page
$idpList = $this->getIdPList();
$idpList = $this->filterList($idpList);
$preferredIdP = $this->getRecommendedIdP();
$idpintersection = array_intersect(array_keys($idpList), $this->getScopedIDPList());
if (sizeof($idpintersection) > 0) {
$idpList = array_intersect_key($idpList, array_fill_keys($idpintersection, null));
}
$idpintersection = array_values($idpintersection);
if (sizeof($idpintersection) == 1) {
$this->log(
'Choice made ['.$idpintersection[0].'] (Redirecting the user back. returnIDParam='.
$this->returnIdParam.')'
);
\SimpleSAML\Utils\HTTP::redirectTrustedURL(
$this->returnURL,
array($this->returnIdParam => $idpintersection[0])
);
}
/*
* Make use of an XHTML template to present the select IdP choice to the user. Currently the supported options
* is either a drop down menu or a list view.
*/
switch ($this->config->getString('idpdisco.layout', 'links')) {
case 'dropdown':
$templateFile = 'selectidp-dropdown.php';
break;
case 'links':
$templateFile = 'selectidp-links.php';
break;
default:
throw new Exception('Invalid value for the \'idpdisco.layout\' option.');
}
$t = new SimpleSAML_XHTML_Template($this->config, $templateFile, 'disco');
$t->data['idplist'] = $idpList;
$t->data['preferredidp'] = $preferredIdP;
$t->data['return'] = $this->returnURL;
$t->data['returnIDParam'] = $this->returnIdParam;
$t->data['entityID'] = $this->spEntityId;
$t->data['urlpattern'] = htmlspecialchars(\SimpleSAML\Utils\HTTP::getSelfURLNoQuery());
$t->data['rememberenabled'] = $this->config->getBoolean('idpdisco.enableremember', false);
$t->show();
}
}

724
lib/SimpleSAML/XHTML/Template.php Executable file
View File

@@ -0,0 +1,724 @@
<?php
/**
* A minimalistic XHTML PHP based template system implemented for SimpleSAMLphp.
*
* @author Andreas Åkre Solberg, UNINETT AS. <andreas.solberg@uninett.no>
* @package SimpleSAMLphp
*/
use JaimePerez\TwigConfigurableI18n\Twig\Environment as Twig_Environment;
use JaimePerez\TwigConfigurableI18n\Twig\Extensions\Extension\I18n as Twig_Extensions_Extension_I18n;
class SimpleSAML_XHTML_Template
{
/**
* The data associated with this template, accessible within the template itself.
*
* @var array
*/
public $data = array();
/**
* A translator instance configured to work with this template.
*
* @var \SimpleSAML\Locale\Translate
*/
private $translator;
/**
* The localization backend
*
* @var \SimpleSAML\Locale\Localization
*/
private $localization;
/**
* The configuration to use in this template.
*
* @var SimpleSAML_Configuration
*/
private $configuration;
/**
* The file to load in this template.
*
* @var string
*/
private $template = 'default.php';
/**
* The twig environment.
*
* @var false|Twig_Environment
*/
private $twig;
/**
* The template name.
*
* @var string
*/
private $twig_template;
/**
* Current module, if any.
*/
private $module;
/**
* A template controller, if any.
*
* Used to intercept certain parts of the template handling, while keeping away unwanted/unexpected hooks. Set
* the 'theme.controller' configuration option to a class that implements the
* SimpleSAML\XHTML\TemplateControllerInterface interface to use it.
*
* @var SimpleSAML\XHTML\TemplateControllerInterface
*/
private $controller;
/**
* Whether we are using a non-default theme or not.
*
* If we are using a theme, this variable holds an array with two keys: "module" and "name", those being the name
* of the module and the name of the theme, respectively. If we are using the default theme, the variable defaults
* to false.
*
* @var bool|array
*/
private $theme;
/**
* Constructor
*
* @param SimpleSAML_Configuration $configuration Configuration object
* @param string $template Which template file to load
* @param string|null $defaultDictionary The default dictionary where tags will come from.
*/
public function __construct(\SimpleSAML_Configuration $configuration, $template, $defaultDictionary = null)
{
$this->configuration = $configuration;
$this->template = $template;
// TODO: do not remove the slash from the beginning, change the templates instead!
$this->data['baseurlpath'] = ltrim($this->configuration->getBasePath(), '/');
// parse module and template name
list($this->module) = $this->findModuleAndTemplateName($template);
// parse config to find theme and module theme is in, if any
list($this->theme['module'], $this->theme['name']) = self::findModuleAndTemplateName(
$this->configuration->getString('theme.use', 'default')
);
// initialize internationalization system
$this->translator = new SimpleSAML\Locale\Translate($configuration, $defaultDictionary);
$this->localization = new \SimpleSAML\Locale\Localization($configuration);
// check if we need to attach a theme controller
$controller = $this->configuration->getString('theme.controller', false);
if ($controller && class_exists($controller) &&
class_implements($controller, '\SimpleSAML\XHTML\TemplateControllerInterface')
) {
$this->controller = new $controller();
}
$this->twig = $this->setupTwig();
}
/**
* Normalize the name of the template to one of the possible alternatives.
*
* @param string $templateName The template name to normalize.
* @return string The filename we need to look for.
*/
private function normalizeTemplateName($templateName)
{
if (strripos($templateName, '.twig')) {
return $templateName;
}
$phppos = strripos($templateName, '.php');
if ($phppos) {
$templateName = substr($templateName, 0, $phppos);
}
$tplpos = strripos($templateName, '.tpl');
if ($tplpos) {
$templateName = substr($templateName, 0, $tplpos);
}
return $templateName.'.twig';
}
/**
* Set up the places where twig can look for templates.
*
* @return Twig_Loader_Filesystem The twig template loader or false if the template does not exist.
* @throws Twig_Error_Loader In case a failure occurs.
*/
private function setupTwigTemplatepaths()
{
$filename = $this->normalizeTemplateName($this->template);
// get namespace if any
list($namespace, $filename) = self::findModuleAndTemplateName($filename);
$this->twig_template = ($namespace !== null) ? '@'.$namespace.'/'.$filename : $filename;
$loader = new \Twig_Loader_Filesystem();
$templateDirs = $this->findThemeTemplateDirs();
if ($this->module) {
$templateDirs[] = array($this->module => $this->getModuleTemplateDir($this->module));
}
if ($this->theme['module']) {
try {
$templateDirs[] = array($this->theme['module'] => $this->getModuleTemplateDir($this->theme['module']));
} catch (\InvalidArgumentException $e) {
// either the module is not enabled or it has no "templates" directory, ignore
}
}
// default, themeless templates are checked last
$templateDirs[] = array(
\Twig_Loader_Filesystem::MAIN_NAMESPACE => $this->configuration->resolvePath('templates')
);
foreach ($templateDirs as $entry) {
$loader->addPath($entry[key($entry)], key($entry));
}
return $loader;
}
/**
* Setup twig.
*/
private function setupTwig()
{
$auto_reload = $this->configuration->getBoolean('template.auto_reload', true);
$cache = $this->configuration->getString('template.cache', false);
// set up template paths
$loader = $this->setupTwigTemplatepaths();
// abort if twig template does not exist
if (!$loader->exists($this->twig_template)) {
return false;
}
// load extra i18n domains
if ($this->module) {
$this->localization->addModuleDomain($this->module);
}
if ($this->theme['module'] !== null && $this->theme['module'] !== $this->module) {
$this->localization->addModuleDomain($this->theme['module']);
}
$options = array(
'cache' => $cache,
'auto_reload' => $auto_reload,
'translation_function' => array('\SimpleSAML\Locale\Translate', 'translateSingularNativeGettext'),
'translation_function_plural' => array('\SimpleSAML\Locale\Translate', 'translatePluralNativeGettext'),
);
// set up translation
if ($this->localization->i18nBackend === \SimpleSAML\Locale\Localization::GETTEXT_I18N_BACKEND) {
$options['translation_function'] = array('\SimpleSAML\Locale\Translate', 'translateSingularGettext');
$options['translation_function_plural'] = array(
'\SimpleSAML\Locale\Translate',
'translatePluralGettext'
);
} // TODO: add a branch for the old SimpleSAMLphp backend
$twig = new Twig_Environment($loader, $options);
$twig->addExtension(new Twig_Extensions_Extension_I18n());
// initialize some basic context
$langParam = $this->configuration->getString('language.parameter.name', 'language');
$twig->addGlobal('languageParameterName', $langParam);
$twig->addGlobal('localeBackend', $this->configuration->getString('language.i18n.backend', 'SimpleSAMLphp'));
$twig->addGlobal('currentLanguage', $this->translator->getLanguage()->getLanguage());
$twig->addGlobal('isRTL', false); // language RTL configuration
if ($this->translator->getLanguage()->isLanguageRTL()) {
$twig->addGlobal('isRTL', true);
}
$queryParams = $_GET; // add query parameters, in case we need them in the template
if (isset($queryParams[$langParam])) {
unset($queryParams[$langParam]);
}
$twig->addGlobal('queryParams', $queryParams);
$twig->addGlobal('templateId', str_replace('.twig', '', $this->normalizeTemplateName($this->template)));
$twig->addGlobal('isProduction', $this->configuration->getBoolean('production', true));
// add a filter for translations out of arrays
$twig->addFilter(
new \Twig_SimpleFilter(
'translateFromArray',
array('\SimpleSAML\Locale\Translate', 'translateFromArray'),
array('needs_context' => true)
)
);
if ($this->controller) {
$this->controller->setUpTwig($twig);
}
return $twig;
}
/**
* Add overriding templates from the configured theme.
*
* @return array An array of module => templatedir lookups.
*/
private function findThemeTemplateDirs()
{
if ($this->theme['module'] === null) { // no module involved
return array();
}
// setup directories & namespaces
$themeDir = \SimpleSAML\Module::getModuleDir($this->theme['module']).'/themes/'.$this->theme['name'];
$subdirs = scandir($themeDir);
if (empty($subdirs)) { // no subdirectories in the theme directory, nothing to do here
// this is probably wrong, log a message
\SimpleSAML\Logger::warning('Empty theme directory for theme "'.$this->theme['name'].'".');
return array();
}
$themeTemplateDirs = array();
foreach ($subdirs as $entry) {
// discard anything that's not a directory. Expression is negated to profit from lazy evaluation
if (!($entry !== '.' && $entry !== '..' && is_dir($themeDir.'/'.$entry))) {
continue;
}
// set correct name for the default namespace
$ns = ($entry === 'default') ? \Twig_Loader_Filesystem::MAIN_NAMESPACE : $entry;
$themeTemplateDirs[] = array($ns => $themeDir.'/'.$entry);
}
return $themeTemplateDirs;
}
/**
* Get the template directory of a module, if it exists.
*
* @return string The templates directory of a module.
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
private function getModuleTemplateDir($module)
{
if (!\SimpleSAML\Module::isModuleEnabled($module)) {
throw new InvalidArgumentException('The module \''.$module.'\' is not enabled.');
}
$moduledir = \SimpleSAML\Module::getModuleDir($module);
// check if module has a /templates dir, if so, append
$templatedir = $moduledir.'/templates';
if (!is_dir($templatedir)) {
throw new InvalidArgumentException('The module \''.$module.'\' has no templates directory.');
}
return $templatedir;
}
/**
* Add the templates from a given module.
*
* Note that the module must be installed, enabled, and contain a "templates" directory.
*
* @param string $module The module where we need to search for templates.
*
* @throws InvalidArgumentException If the module is not enabled or it has no templates directory.
*/
public function addTemplatesFromModule($module)
{
$dir = $this->getModuleTemplateDir($module);
/** @var Twig_Loader_Filesystem $loader */
$loader = $this->twig->getLoader();
$loader->addPath($dir, $module);
}
/**
* Generate an array for its use in the language bar, indexed by the ISO 639-2 codes of the languages available,
* containing their localized names and the URL that should be used in order to change to that language.
*
* @return array The array containing information of all available languages.
*/
private function generateLanguageBar()
{
$languages = $this->translator->getLanguage()->getLanguageList();
$langmap = null;
if (count($languages) > 1) {
$parameterName = $this->getTranslator()->getLanguage()->getLanguageParameterName();
$langmap = array();
foreach ($languages as $lang => $current) {
$lang = strtolower($lang);
$langname = $this->translator->getLanguage()->getLanguageLocalizedName($lang);
$url = false;
if (!$current) {
$url = htmlspecialchars(\SimpleSAML\Utils\HTTP::addURLParameters(
'',
array($parameterName => $lang)
));
}
$langmap[$lang] = array(
'name' => $langname,
'url' => $url,
);
}
}
return $langmap;
}
/**
* Set some default context
*/
private function twigDefaultContext()
{
// show language bar by default
if (!isset($this->data['hideLanguageBar'])) {
$this->data['hideLanguageBar'] = false;
}
// get languagebar
$this->data['languageBar'] = null;
if ($this->data['hideLanguageBar'] === false) {
$languageBar = $this->generateLanguageBar();
if (is_null($languageBar)) {
$this->data['hideLanguageBar'] = true;
} else {
$this->data['languageBar'] = $languageBar;
}
}
// assure that there is a <title> and <h1>
if (isset($this->data['header']) && !isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = $this->data['header'];
}
if (!isset($this->data['pagetitle'])) {
$this->data['pagetitle'] = 'SimpleSAMLphp';
}
}
/**
* Show the template to the user.
*/
public function show()
{
if ($this->twig !== false) {
$this->twigDefaultContext();
if ($this->controller) {
$this->controller->display($this->data);
}
echo $this->twig->render($this->twig_template, $this->data);
} else {
$filename = $this->findTemplatePath($this->template);
require($filename);
}
}
/**
* Find module the template is in, if any
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return array An array with the name of the module and template
*/
private function findModuleAndTemplateName($template)
{
$tmp = explode(':', $template, 2);
return (count($tmp) === 2) ? array($tmp[0], $tmp[1]) : array(null, $tmp[0]);
}
/**
* Find template path.
*
* This function locates the given template based on the template name. It will first search for the template in
* the current theme directory, and then the default theme.
*
* The template name may be on the form <module name>:<template path>, in which case it will search for the
* template file in the given module.
*
* @param string $template The relative path from the theme directory to the template file.
*
* @return string The absolute path to the template file.
*
* @throws Exception If the template file couldn't be found.
*/
private function findTemplatePath($template, $throw_exception = true)
{
assert(is_string($template));
list($templateModule, $templateName) = $this->findModuleAndTemplateName($template);
$templateModule = ($templateModule !== null) ? $templateModule : 'default';
// first check the current theme
if ($this->theme['module'] !== null) {
// .../module/<themeModule>/themes/<themeName>/<templateModule>/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($this->theme['module']).
'/themes/'.$this->theme['name'].'/'.$templateModule.'/'.$templateName;
} elseif ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($templateModule).'/templates/'.$templateName;
} else {
// .../templates/<theme>/<templateName>
$filename = $this->configuration->getPathValue('templatedir', 'templates/').$templateName;
}
if (file_exists($filename)) {
return $filename;
}
// not found in current theme
\SimpleSAML\Logger::debug(
$_SERVER['PHP_SELF'].' - Template: Could not find template file ['.$template.'] at ['.
$filename.'] - now trying the base template'
);
// try default theme
if ($templateModule !== 'default') {
// .../module/<templateModule>/templates/<templateName>
$filename = \SimpleSAML\Module::getModuleDir($templateModule).'/templates/'.$templateName;
} else {
// .../templates/<templateName>
$filename = $this->configuration->getPathValue('templatedir', 'templates/').'/'.$templateName;
}
if (file_exists($filename)) {
return $filename;
}
// not found in default template
if ($throw_exception) {
// log error and throw exception
$error = 'Template: Could not find template file ['.$template.'] at ['.$filename.']';
\SimpleSAML\Logger::critical($_SERVER['PHP_SELF'].' - '.$error);
throw new Exception($error);
} else {
// missing template expected, return NULL
return null;
}
}
/**
* Return the internal translator object used by this template.
*
* @return \SimpleSAML\Locale\Translate The translator that will be used with this template.
*/
public function getTranslator()
{
return $this->translator;
}
/**
* Get the current instance of Twig in use.
*
* @return false|Twig_Environment The Twig instance in use, or false if Twig is not used.
*/
public function getTwig()
{
return $this->twig;
}
/*
* Deprecated methods of this interface, all of them should go away.
*/
/**
* @param $name
*
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getAttributeTranslation($name)
{
return $this->translator->getAttributeTranslation($name);
}
/**
* @return string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguage()
* instead.
*/
public function getLanguage()
{
return $this->translator->getLanguage()->getLanguage();
}
/**
* @param $language
* @param bool $setLanguageCookie
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguage()
* instead.
*/
public function setLanguage($language, $setLanguageCookie = true)
{
$this->translator->getLanguage()->setLanguage($language, $setLanguageCookie);
}
/**
* @return null|string
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::getLanguageCookie()
* instead.
*/
public static function getLanguageCookie()
{
return \SimpleSAML\Locale\Language::getLanguageCookie();
}
/**
* @param $language
*
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Language::setLanguageCookie()
* instead.
*/
public static function setLanguageCookie($language)
{
\SimpleSAML\Locale\Language::setLanguageCookie($language);
}
/**
* Wraps Language->getLanguageList
*/
private function getLanguageList()
{
return $this->translator->getLanguage()->getLanguageList();
}
/**
* @param $tag
*
* @return array
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::getTag() instead.
*/
public function getTag($tag)
{
return $this->translator->getTag($tag);
}
/**
* Temporary wrapper for \SimpleSAML\Locale\Translate::getPreferredTranslation().
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::getPreferredTranslation() instead.
*/
public function getTranslation($translations)
{
return $this->translator->getPreferredTranslation($translations);
}
/**
* Includes a file relative to the template base directory.
* This function can be used to include headers and footers etc.
*
*/
private function includeAtTemplateBase($file)
{
$data = $this->data;
$filename = $this->findTemplatePath($file);
include($filename);
}
/**
* Wraps Translate->includeInlineTranslation()
*
* @see \SimpleSAML\Locale\Translate::includeInlineTranslation()
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeInlineTranslation() instead.
*/
public function includeInlineTranslation($tag, $translation)
{
$this->translator->includeInlineTranslation($tag, $translation);
}
/**
* @param $file
* @param null $otherConfig
*
* @deprecated This method will be removed in SSP 2.0. Please use
* \SimpleSAML\Locale\Translate::includeLanguageFile() instead.
*/
public function includeLanguageFile($file, $otherConfig = null)
{
$this->translator->includeLanguageFile($file, $otherConfig);
}
/**
* Wrap Language->isLanguageRTL
*/
private function isLanguageRTL()
{
return $this->translator->getLanguage()->isLanguageRTL();
}
/**
* Merge two translation arrays.
*
* @param array $def The array holding string definitions.
* @param array $lang The array holding translations for every string.
*
* @return array The recursive merge of both arrays.
* @deprecated This method will be removed in SimpleSAMLphp 2.0. Please use array_merge_recursive() instead.
*/
public static function lang_merge($def, $lang)
{
foreach ($def as $key => $value) {
if (array_key_exists($key, $lang)) {
$def[$key] = array_merge($value, $lang[$key]);
}
}
return $def;
}
/**
* Behave like Language->noop to mark a tag for translation but actually do it later.
*
* @see \SimpleSAML\Locale\Translate::noop()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::noop() instead.
*/
public static function noop($tag)
{
return $tag;
}
/**
* Wrap Language->t to translate tag into the current language, with a fallback to english.
*
* @see \SimpleSAML\Locale\Translate::t()
* @deprecated This method will be removed in SSP 2.0. Please use \SimpleSAML\Locale\Translate::t() instead.
*/
public function t(
$tag,
$replacements = array(),
$fallbackdefault = true,
$oldreplacements = array(),
$striptags = false
) {
return $this->translator->t($tag, $replacements, $fallbackdefault, $oldreplacements, $striptags);
}
}

Some files were not shown because too many files have changed in this diff Show More