pax_global_header00006660000000000000000000000064146276051350014523gustar00rootroot0000000000000052 comment=f49f5353fdd6e27bf754bd674dc566b5ed711b39 osutil-1.5.0/000077500000000000000000000000001462760513500130455ustar00rootroot00000000000000osutil-1.5.0/AUTHORS.md000066400000000000000000000006071462760513500145170ustar00rootroot00000000000000# Authors This is the official list of authors for copyright purposes. This file is distinct from the 'CONTRIBUTORS' file. See the latter for an explanation. Names should be added to this file as: Name or Organization / (url address) (The email address is not required for organizations) Please keep the list sorted. ## Code * Jonas mg (https://github.com/tredoe) osutil-1.5.0/CONTRIBUTORS.md000066400000000000000000000011271462760513500153250ustar00rootroot00000000000000# Contributors This is the official list of people who can contribute (and typically have contributed) to the repository. The 'AUTHORS' file lists the copyright holders; this file lists people. For example, the employees of an organization are listed here but not in 'AUTHORS', because the organization holds the copyright. Names should be added to this file as: Name / (url address) Please keep the list sorted. ## Code * Jonas mg (https://github.com/tredoe) * DJ Gregor * Jeramey Crawford * Misha Seltzer (https://github.com/mishas) osutil-1.5.0/Doc/000077500000000000000000000000001462760513500135525ustar00rootroot00000000000000osutil-1.5.0/Doc/info.txt000066400000000000000000000033141462760513500152470ustar00rootroot00000000000000 + Library for Unix users and groups in Rust https://github.com/ogham/rust-users https://gitlab.com/core-utils/users_native/blob/master/src/lib.rs * * * http://www.manpages.info/freebsd/group.5.html http://www.manpages.info/freebsd/passwd.5.html FreeBSD: /etc/login.conf passwd_format /etc/passwd /etc/master.passwd * * * Command getent: It retrieves the contents for passwd, shadow, gshadow and a bunch more and accesses LDAP and others when present. If the user doesn't have the required privileges, it returns nothing. It always uses the "right" information source (say you do use LDAP - it'll report the passwd data from LDAP instead of /etc/passwd). this really needs to be done via the system libraries to be complete. It looks to cover the basics fairly well (you didn't even forget about gshadow like most people), but I wouldn't try to go much further down this path (e.g. don't parse nsswitch.conf yourself, and try to resolve usernames elsewhere). Take a look at python-libuser to see what's required for a more complete interface. https://fedorahosted.org/libuser/ It's a crufty old system, and you really have to go through the standard C libraries to make sure your library follows the same rules as everything else that resolves user and group information. * * * See command "chage -l [user]" * * * Add tests for functions related to changeType in file shadow.go * * * http://nerds-central.blogspot.ca/2007/09/how-to-get-sid-for-group-or-user-by.html http://msdn.microsoft.com/en-us/library/windows/desktop/aa379159%28v=vs.85%29.aspx http://msdn.microsoft.com/en-us/library/windows/desktop/ms721798%28v=vs.85%29.aspx http://msdn.microsoft.com/en-us/library/windows/desktop/ms721799%28v=vs.85%29.aspx osutil-1.5.0/LICENSE-MPL.txt000066400000000000000000000405271462760513500153260ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. osutil-1.5.0/README.md000066400000000000000000000023001462760513500143170ustar00rootroot00000000000000# osutil Access to operating system functionality dependent of every platform and utility packages for the Shell. + config/env: set persistent environment variables + config/shconf: parser and scanner for the configuration in format shell-variable + distro: detects the Linux distribution + file: common operations in files + pkg: basic operations for the management of packages in operating systems + sh: interprets a command line like it is done in the Bash shell + user: provides access to UNIX users database in local files + user/crypt: password hashing used in UNIX [Documentation online](http://godoc.org/github.com/tredoe/osutil) ## Testing `go test ./...` `sudo env PATH=$PATH go test -v ./...` 'sudo' command is necessary to copy the files '/etc/{passwd,group,shadow,gshadow}' to the temporary directory, where the tests are run. Also, it uses 'sudo' to check the package manager, at installing and removing the package 'mtr-tiny'. ## License The source files are distributed under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/), unless otherwise noted. Please read the [FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html) if you have further questions regarding the license. osutil-1.5.0/TODO.md000066400000000000000000000013641462760513500141400ustar00rootroot00000000000000## pkg Check the error code returned at testing. ## file edit.go: the comment character used by default Copy with options: (overwrite) type option int const ( _ option = iota ARCHIVE // preserve all attributes BACKUP // make a backup of each existing destination file ATTRIBUTES // only copy attributes HLINK // make hard link instead of copying SLINK // make symbolic links instead of copying DEREFERENCE // always follow symbolic links in SOURCE RECURSIVE // copy directories recursively UPDATE // copy only when the source file is newer than the destination file // or when the destination file is missing ) ## sh doc. about the usage of Installer since it uses a logger into a directory installed by such tool. osutil-1.5.0/config/000077500000000000000000000000001462760513500143125ustar00rootroot00000000000000osutil-1.5.0/config/env/000077500000000000000000000000001462760513500151025ustar00rootroot00000000000000osutil-1.5.0/config/env/Info.md000066400000000000000000000056271462760513500163310ustar00rootroot00000000000000Persistent variables shell ========================== Environment variables are a per process thing. So something needs to store the state to disk and load it in to it's environment and pass that environment on to it's child processes. This is generally done by the shell, I don't think there is a cross shell way to tell a shell to persist an environment variable to it's configuration. https://help.ubuntu.com/community/EnvironmentVariables ## Persistent environment variables The names of environment variables are case sensitive. It is a common practice to name all environment variables with only English capital letters and underscore (_) signs. Note: The shell techniques explained in the following sections apply to the Bourne Shell family of command line shells, which includes sh, ksh, and bash, which is the default shell shipped with Ubuntu. The commands may be different on other shells such as csh. ### System-wide environment variables Environment variable settings that affect the system as a whole (rather than just a particular user) should not be placed in any of the many system-level scripts that get executed when the system or the desktop session are loaded, but into /etc/environment - This file is specifically meant for system-wide environment variable settings. It is not a script file, but rather consists of assignment expressions, one per line. Specifically, this file stores the system-wide locale and path settings. Note: Any variables added to these locations will not be reflected when invoking them with a sudo command, as sudo has a default policy of resetting the Environment and setting a secure path (this behavior is defined in /etc/sudoers). As a workaround, you can use "sudo su" that will provide a shell with root privileges but retaining any modified PATH variables. Note: When dealing with end-user/home desktop systems may be appropriate to place settings in the user's ~/.pam_environment files discussed above rather than the system-wide ones, since those files do not require one to utilize root privileges in order to edit and are easily moved between systems. Note: Some systems now use an envvar.sh placed in the /etc/profile.d/ directory to set system wide environment strings. ### Session-wide environment variables Environment variable settings that should affect just a particular user (rather than the system as a whole) should be set into: ~/.pam_environment - This file is specifically meant for setting a user's environment. It is not a script file, but rather consists of assignment expressions, one per line. PATH DEFAULT=${PATH}:${HOME}/MyPrograms Note: Using .pam_environment requires a re-login in order to initialize the variables. Restarting just the terminal is not sufficient to be able to use the variables. If you are using KDE, see http://userbase.kde.org/Session_Environment_Variables/en ## Windows http://support.microsoft.com/kb/310519 osutil-1.5.0/config/env/env.go000066400000000000000000000171201462760513500162220ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package env implements the setting of persistent environment variables. // // The environment variables must be named with only English capital letters and // underscore signs (_). package env import ( "bytes" "errors" "fmt" "os" "path" "github.com/tredoe/osutil/config/shconf" "github.com/tredoe/osutil/file" "github.com/tredoe/osutil/user" ) var _IS_ROOT bool func init() { if os.Getuid() == 0 { _IS_ROOT = true } } // == Errors var ErrNoRoot = errors.New("you have to be Root") // NoShellError represents an account without shell. type NoShellError string func (e NoShellError) Error() string { return "shell not found for user: " + string(e) } // NoHomeError represents an account without home directory. type NoHomeError string func (e NoHomeError) Error() string { return "home directory not found for user: " + string(e) } var errKey = errors.New("environment variables must use only English capital" + " letters and underscore signs") // === // Families of shell commands. var ( shFamily = []string{"bash", "ksh", "sh"} cshFamily = []string{"tcsh", "csh"} ) // shellType represents a type of shell. type shellType int const ( _SHELL_SH shellType = 1 + iota _SHELL_CSH ) // == Settings for each Shell family // var systemFile = []string{ _SHELL_SH: "/etc/environment", //_SHELL_CSH: "", } var userFile = []string{ _SHELL_SH: ".pam_environment", //_SHELL_CSH: "", } // To set environment variables that affect your whole session, KDE will execute // any script it finds in '$HOME/.kde/env' whose filename ends in '.sh', and it // will maintain all the environment variables set by them. var kdeFile = ".kde/env" // settings represents the files of configuration for an user, according to the shell. type settings struct { global string // System wide user string kde string useKDE bool } // Settings files of the caller. var _SETTINGS settings // Sets the settings files of the caller. func init() { var err error _SETTINGS, err = getSettingsForUid(os.Getuid()) if err != nil { panic(err) } } // getSettingsForUid returns the settings files of the given user id. func getSettingsForUid(id int) (settings, error) { u, err := user.LookupUID(id) if err != nil { return settings{}, err } if u.Shell == "" { return settings{}, NoShellError(u.Name) } if u.Dir == "" { return settings{}, NoHomeError(u.Name) } shell := path.Base(u.Shell) _settings := settings{ global: systemFile[_SHELL_SH], user: path.Join(u.Dir, userFile[_SHELL_SH]), kde: path.Join(u.Dir, kdeFile), } info, err := os.Stat(_settings.kde) if err == nil && info.IsDir() { _settings.useKDE = true } for _, v := range shFamily { if v == shell { return _settings, nil } } /*for _, v := range cshFamily { if v == shell { return _settings, nil } }*/ return settings{}, fmt.Errorf("shell unsopported: %s", shell) } // == Set variables // // _Set sets the value named by the key in the given filename. func _Set(filename, key, value string) error { // Check if the key is already used. conf, err := shconf.ParseFile(filename) if err != nil { if err != os.ErrNotExist { return err } println("ErrNotExist") //TODO: remove } else { if _, err = conf.Get(key); err != shconf.ErrKey { panic("OPS") } } return file.AppendString(filename, key+string(conf.Separator())+value) } // _MSet sets multiple values named by the keys in the given filename. func _MSet(filename string, keys, values []string) error { if len(keys) != len(values) { return fmt.Errorf("number of keys is different to number of values") } // Check if the key is already used. conf, err := shconf.ParseFile(filename) if err != nil { if err != os.ErrNotExist { return err } println("ErrNotExist") //TODO: remove } var buf bytes.Buffer for i, key := range keys { if _, err = conf.Get(key); err != nil { continue // TODO: log key already set. } buf.WriteString(key) buf.Write(conf.Separator()) buf.WriteString(values[i]) buf.WriteByte('\n') } return file.Append(filename, buf.Bytes()) } // == Set session-wide variables // Set sets the value of the environment variable named by the key that // affects the current user. // It returns an error, if any. func Set(key, value string) error { err := _Set(_SETTINGS.user, key, value) if err != nil { return err } if _SETTINGS.useKDE { return _Set(_SETTINGS.kde, key, value) } return nil } // MSet sets multiple values of the environment variables named by the keys // that affects the current user. // It returns an error, if any. func MSet(keys, values []string) error { err := _MSet(_SETTINGS.user, keys, values) if err != nil { return err } if _SETTINGS.useKDE { return _MSet(_SETTINGS.kde, keys, values) } return nil } // SetForUid sets the value of the environment variable named by the key that // affects a particular user. // It returns an error, if any. func SetForUid(id int, key, value string) error { _settings, err := getSettingsForUid(id) if err != nil { return err } if err = _Set(_settings.user, key, value); err != nil { return err } if _settings.useKDE { return _Set(_settings.kde, key, value) } return nil } // MSetForUid sets multiple values of the environment variables named by the // keys that affects a particular user. // It returns an error, if any. func MSetForUid(id int, keys, values []string) error { _settings, err := getSettingsForUid(id) if err != nil { return err } if err = _MSet(_settings.user, keys, values); err != nil { return err } if _settings.useKDE { return _MSet(_settings.kde, keys, values) } return nil } // == Set system-wide variables // Setsys sets the value of the environment variable named by the key that // affects the system as a whole. You must be Root. // It returns an error, if any. func Setsys(key, value string) error { if !_IS_ROOT { return ErrNoRoot } return _Set(_SETTINGS.global, key, value) } // MSetsys sets multiple values of the environment variables named by the keys // that affects the system as a whole. You must be Root. // It returns an error, if any. func MSetsys(keys, values []string) error { if !_IS_ROOT { return ErrNoRoot } return _MSet(_SETTINGS.global, keys, values) } // SetsysForUid sets the value of the environment variable named by the key that // affects the system as a whole. You must be Root. // It returns an error, if any. func SetsysForUid(id int, key, value string) error { if !_IS_ROOT { return ErrNoRoot } _settings, err := getSettingsForUid(id) if err != nil { return err } return _Set(_settings.global, key, value) } // MSetsysForUid sets multiple values of the environment variables named by the // keys that affects the system as a whole. You must be Root. // It returns an error, if any. func MSetsysForUid(id int, keys, values []string) error { if !_IS_ROOT { return ErrNoRoot } _settings, err := getSettingsForUid(id) if err != nil { return err } return _MSet(_settings.global, keys, values) } // == Unset variables // // _Unset unsets the key in the given filename. /*func _Unset(filename, key string) error { }*/ // == Utility // // It is a common practice to name all environment variables with only English // capital letters and underscore (_) signs. // checkKey reports whether the key uses English capital letters and underscore // signs. func checkKey(s string) { for _, char := range s { if (char < 'A' || char > 'Z') && char != '_' { panic(errKey) } } } osutil-1.5.0/config/env/env_test.go000066400000000000000000000013411462760513500172570ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package env import "testing" func TestCheckKey(t *testing.T) { caught := false defer func() { if x := recover(); x != nil { if x != errKey { t.Fatal("expected to get error: errKey") } caught = true } }() goodKey := "FOO_BAR" checkKey(goodKey) if caught == true { t.Fatalf("expected to don't get a call to panic with key %q", goodKey) } badKey := goodKey + "_a" checkKey(badKey) if caught == false { t.Fatalf("expected to get a call to panic with key %q", badKey) } } osutil-1.5.0/config/shconf/000077500000000000000000000000001462760513500155725ustar00rootroot00000000000000osutil-1.5.0/config/shconf/scan.go000066400000000000000000000173751462760513500170620ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package shconf import ( "bufio" "bytes" "io" "strconv" "unicode" "unicode/utf8" ) type extraCharError int func (e extraCharError) Error() string { return strconv.Itoa(int(e)) + ": extra character/s after of the value" } type keyError int func (e keyError) Error() string { return strconv.Itoa(int(e)) + ": key not found" } type noASCIIKeyError int func (e noASCIIKeyError) Error() string { return strconv.Itoa(int(e)) + ": the key only must to have ASCII characters" } type invalidCharError int func (e invalidCharError) Error() string { return strconv.Itoa(int(e)) + ": key with invalid character" } type openQuoteError int func (e openQuoteError) Error() string { return strconv.Itoa(int(e)) + ": the quote in the value is not closed" } type valueError int func (e valueError) Error() string { return strconv.Itoa(int(e)) + ": value not found" } // * * * // separator is the character used like separator, by default. var separator = []byte{'='} // Scanner provides a convenient interface for reading data such as a file of // lines of text in format key-value. Successive calls to the Scan method will // step through the 'tokens' of a file, skipping the bytes between the tokens. // // The valid characters for the keys are: [a-zA-Z_.] // // Scanning stops unrecoverably at EOF, the first I/O error, or a token too // large to fit in the buffer. type Scanner struct { buf *bufio.Reader // Character/s used to separate the value from key. // It is only get in the first call to "Scan()". separator []byte key []byte value []byte err error line int // Number of line being scanned } // NewScanner returns a new Scanner to read from r, with the option to skip the // keys disabled. func NewScanner(r io.Reader) *Scanner { return &Scanner{buf: bufio.NewReader(r)} } // Scan advances the Scanner to the next tokens, which will then be available // through the Bytes or Text method. It returns false when the scan stops, either // by reaching the end of the input or an error. After Scan returns false, the Err // method will return any error that occurred during scanning, except that if it // was io.EOF, Err will return nil. func (s *Scanner) Scan() bool { var thisRune rune var n int var err error for s.line++; ; s.line++ { // Skip leading spaces. for thisRune, n, err = s.buf.ReadRune(); ; thisRune, _, err = s.buf.ReadRune() { if err != nil { s.err = err return false } if thisRune == '\n' { s.line++ continue } if !unicode.IsSpace(thisRune) { break } } // Skip line comment and section. if thisRune == '#' || thisRune == '[' { if _, err = s.buf.ReadBytes('\n'); err != nil { s.err = err return false } continue } break } if thisRune == '=' { s.err = keyError(s.line) return false } var key, value, separator []rune // Get key for ; ; thisRune, n, err = s.buf.ReadRune() { if err != nil { if err != io.EOF { s.err = err } else { s.err = valueError(s.line) } return false } // The separator charater could the the equal sign or space. if thisRune == '=' || unicode.IsSpace(thisRune) { break } if n > 1 { s.err = noASCIIKeyError(s.line) return false } if !isValidChar(thisRune) { s.err = invalidCharError(s.line) return false } key = append(key, thisRune) } // Skip spaces before and after of the separator character. if unicode.IsSpace(thisRune) { separator = append(separator, thisRune) for thisRune, _, err = s.buf.ReadRune(); ; thisRune, _, err = s.buf.ReadRune() { if err != nil { if err != io.EOF { s.err = err } else { s.err = valueError(s.line) } return false } if !unicode.IsSpace(thisRune) { break } separator = append(separator, thisRune) } } if thisRune == '=' { separator = append(separator, thisRune) if thisRune, _, err = s.buf.ReadRune(); err != nil { if err != io.EOF { s.err = err } else { s.err = valueError(s.line) } return false } } if unicode.IsSpace(thisRune) { separator = append(separator, thisRune) for thisRune, _, err = s.buf.ReadRune(); ; thisRune, _, err = s.buf.ReadRune() { if err != nil { if err != io.EOF { s.err = err } else { s.err = valueError(s.line) } return false } if !unicode.IsSpace(thisRune) { break } separator = append(separator, thisRune) } } // Get value var valueIsString, valueInDQuote bool var lastRune rune if thisRune == '"' { // The value is between double quotes valueIsString = true valueInDQuote = true } else if thisRune == '\'' { // between single quotes valueIsString = true } else { value = append(value, thisRune) } for thisRune, _, err = s.buf.ReadRune(); ; thisRune, _, err = s.buf.ReadRune() { if err != nil { if err != io.EOF { s.err = err return false } if valueIsString { s.err = openQuoteError(s.line) return false } s.err = err break } if valueIsString { if valueInDQuote { if thisRune == '"' && lastRune != '\\' { break } } else if thisRune == '\'' && lastRune != '\\' { break } lastRune = thisRune // To checking if it is a quote escaped. value = append(value, thisRune) } else { if unicode.IsSpace(thisRune) { break } value = append(value, thisRune) } } // Sanity check if thisRune != '\n' && thisRune != '\r' { doCheck := true last, _, err := s.buf.ReadLine() if err != nil { if err != io.EOF { s.err = err return false } else { doCheck = false } } if doCheck { for _, char := range last { if unicode.IsSpace(rune(char)) { continue } else if char == '#' { break } else { s.err = extraCharError(s.line) return false } } } } // Store key and value var bufKey, bufValue, bufSep bytes.Buffer var bytes []byte for _, r := range key { bytes = make([]byte, utf8.RuneLen(r)) utf8.EncodeRune(bytes, r) bufKey.Write(bytes) } s.key = bufKey.Bytes() for _, r := range value { bytes = make([]byte, utf8.RuneLen(r)) utf8.EncodeRune(bytes, r) bufValue.Write(bytes) } s.value = bufValue.Bytes() // Store separator character if s.separator == nil { for _, r := range separator { bytes = make([]byte, utf8.RuneLen(r)) utf8.EncodeRune(bytes, r) bufSep.Write(bytes) } s.separator = bufSep.Bytes() } return true } // Bytes returns the most recents tokens generated by a call to Scan. The // underlying array may point to data that will be overwritten by a subsequent // call to Scan. It does no allocation. func (s *Scanner) Bytes() (key, value []byte) { return s.key, s.value } // Text returns the most recents tokens generated by a call to Scan as a newly // allocated string holding its bytes. func (s *Scanner) Text() (key, value string) { return string(s.key), string(s.value) } // Err returns the first non-EOF error that was encountered by the Scanner. func (s *Scanner) Err() error { if s.err != io.EOF { return s.err } return nil } // Separator returns the character/s used to separate the key from the value. // // The separator is got in the first call to "Scan()"; if it has not been // called, this makes it explicitly but panics when there is any error. func (s *Scanner) Separator() []byte { if s.separator == nil { if found := s.Scan(); !found { if err := s.Err(); err != nil { panic(err) } return separator } } return s.separator } // == Utility func isValidChar(r rune) bool { if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { return true } switch r { case '_', '.': return true default: return false } } osutil-1.5.0/config/shconf/scan_test.go000066400000000000000000000034471462760513500201140ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package shconf import ( "bytes" "testing" ) var scanTests = []struct { in string key string value string }{ {" \n # qwe", "", ""}, {" \n [ qwe ]\n ABC=def", "ABC", "def"}, {" \n # qwe \n ABC=def", "ABC", "def"}, {"ABC=def", "ABC", "def"}, {" ABC = def ", "ABC", "def"}, {" \n FOO=bar ", "FOO", "bar"}, // 5 {`FOO="bar"`, "FOO", "bar"}, {"FOO='bar'", "FOO", "bar"}, {"foo_a='bar'", "foo_a", "bar"}, {"foo.b = 'bar'", "foo.b", "bar"}, } func TestScanKeys(t *testing.T) { for n, test := range scanTests { s := NewScanner(bytes.NewBufferString(test.in)) s.Scan() k, v := s.Text() if test.key != k { t.Errorf("#%d: key: expected %q, got %q\n", n, test.key, k) } if test.value != v { t.Errorf("#%d: value: expected %q, got %q\n", n, test.value, v) } if err := s.Err(); err != nil { t.Errorf("#%d: %v", n, err) } if s.separator != nil && len(separator) == len(s.separator) { if separator[0] != s.separator[0] { t.Errorf("#%d: separator: expected %q, got %q\n", n, separator, s.separator) } } } } var errorTests = []struct { in string err error }{ {"FOO=bar z", extraCharError(1)}, {" =bar", keyError(1)}, {"FOO", valueError(1)}, {" FOO=", valueError(1)}, {"FOO_€=bar", noASCIIKeyError(1)}, {`FOO="bar`, openQuoteError(1)}, {`FOO='bar`, openQuoteError(1)}, } func TestErrors(t *testing.T) { var err error for n, test := range errorTests { s := NewScanner(bytes.NewBufferString(test.in)) s.Scan() err = s.Err() if test.err != err { t.Errorf("#%d: expected error %q, got %v\n", n, test.err, err) } } } osutil-1.5.0/config/shconf/shconf.go000066400000000000000000000114051462760513500174020ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package shconf implements a parser and scanner for the configuration in // format shell-variable. // // The configuration file consists on entries with the format: // "key" [separator] "value" // The comments are indicated by "#" at the beginning of a line and upon the keys. package shconf import ( "errors" "fmt" "os" "reflect" "strconv" "sync" "github.com/tredoe/osutil/file" ) var ( ErrKey = errors.New("key not found") ErrStructPtr = errors.New("argument must be a pointer to struct") ) // A TypeError represents the type no supported in function Unmarshal. type TypeError string func (e TypeError) Error() string { return "type no supported: " + string(e) } // A Config represents the configuration. type Config struct { sync.RWMutex data map[string]string // key: value separator []byte filename string } // ParseFile creates a new Config and parses the file configuration from the // named file. func ParseFile(name string) (*Config, error) { file, err := os.Open(name) if err != nil { return nil, err } cfg := &Config{ sync.RWMutex{}, make(map[string]string), make([]byte, 0), file.Name(), } cfg.Lock() defer cfg.Unlock() defer file.Close() gotSeparator := false s := NewScanner(file) for found := s.Scan(); found; found = s.Scan() { if found { k, v := s.Text() cfg.data[k] = v if !gotSeparator { cfg.separator = s.Separator() gotSeparator = true } continue } else if s.Err() != nil { return nil, err } } return cfg, nil } // Print outputs the keys and values parsed. func (c *Config) Print() { fmt.Println(c.filename) for k, v := range c.data { fmt.Printf("\t%s: %s\n", k, v) } } // Separator returns the character/s used to separate the key from the value. func (c *Config) Separator() []byte { return c.separator } // Unmarshal assigns values into the pointer to struct given by out, for the // keys found. // // The valid types in the struct fields for the matched keys are: bool, int, // uint, float64, string. // // The errors that Unmarshal return can be ErrStructPtr or TypeError. func (c *Config) Unmarshal(out interface{}) error { valueof := reflect.ValueOf(out) if valueof.Kind() != reflect.Ptr { return ErrStructPtr } valueof = valueof.Elem() if valueof.Kind() != reflect.Struct { return ErrStructPtr } typeof := valueof.Type() for i := 0; i < valueof.NumField(); i++ { fieldT := typeof.Field(i) fieldV := valueof.Field(i) if value, found := c.data[fieldT.Name]; found { switch fieldV.Kind() { case reflect.Bool: v, _ := strconv.ParseBool(value) fieldV.SetBool(v) case reflect.Int: v, _ := strconv.ParseInt(value, 10, 0) fieldV.SetInt(v) case reflect.Uint: v, _ := strconv.ParseUint(value, 10, 0) fieldV.SetUint(v) case reflect.Float64: v, _ := strconv.ParseFloat(value, 64) fieldV.SetFloat(v) /*case reflect.Float32: v, _ := strconv.ParseFloat(value, 32) fieldV.SetFloat(v)*/ case reflect.String: fieldV.SetString(value) default: return TypeError(fieldV.Kind().String()) } } } return nil } // * * * // Get returns the string value for a given key. func (c *Config) Get(key string) (string, error) { if value, found := c.data[key]; found { return value, nil } return "", ErrKey } // Getbool returns the boolean value for a given key. func (c *Config) Getbool(key string) (bool, error) { if value, found := c.data[key]; found { return strconv.ParseBool(value) } return false, ErrKey } // Getint returns the integer value for a given key. func (c *Config) Getint(key string) (int, error) { if value, found := c.data[key]; found { v, err := strconv.ParseInt(value, 10, 0) return int(v), err } return 0, ErrKey } // Getuint returns the unsigned integer value for a given key. func (c *Config) Getuint(key string) (uint, error) { if value, found := c.data[key]; found { v, err := strconv.ParseUint(value, 10, 0) return uint(v), err } return 0, ErrKey } // Getfloat returns the float value for a given key. func (c *Config) Getfloat(key string) (float64, error) { if value, found := c.data[key]; found { return strconv.ParseFloat(value, 64) } return 0, ErrKey } // Set writes a new value for key. func (c *Config) Set(key, value string) error { c.Lock() defer c.Unlock() if _, found := c.data[key]; !found { return ErrKey } separator := string(c.Separator()) replAt := []file.ReplacerAtLine{ {key + separator, separator + ".*", separator + value}, } if err := file.ReplaceAtLine(c.filename, replAt); err != nil { return err } c.data[key] = value return nil } osutil-1.5.0/config/shconf/shconf_test.go000066400000000000000000000047471462760513500204540ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package shconf import ( "bufio" "fmt" "io/ioutil" "os" "path/filepath" "testing" ) var testdata = []struct { k string v string }{ {"BOOL", "true"}, {"INT", "-2"}, {"UINT", "5"}, {"FLOAT", "3.3"}, {"STRING", "small"}, } type conf struct { BOOL bool INT int UINT uint FLOAT float64 STRING string } func TestParseFile(t *testing.T) { // == Create temporary file file, _ := ioutil.TempFile("", "test") buf := bufio.NewWriter(file) buf.WriteString("# main comment\n\n") buf.WriteString(fmt.Sprintf("%s=%s\n", testdata[0].k, testdata[0].v)) buf.WriteString(fmt.Sprintf("%s=%s\n\n", testdata[1].k, testdata[1].v)) buf.WriteString(fmt.Sprintf("%s=%s\n\n", testdata[2].k, testdata[2].v)) buf.WriteString("# Another comment\n") buf.WriteString(fmt.Sprintf("%s=%s\n", testdata[3].k, testdata[3].v)) buf.WriteString(fmt.Sprintf("%s=%s\n", testdata[4].k, testdata[4].v)) buf.Flush() file.Close() defer func() { files, err := filepath.Glob(file.Name() + "*") if err != nil { t.Error(err) return } for _, v := range files { if err = os.Remove(v); err != nil { t.Error(err) } } }() // == Parser conf_ok := &conf{} conf_bad := conf{} cfg, err := ParseFile(file.Name()) if err != nil { t.Fatal(err) } for k, _ := range cfg.data { switch k { case "BOOL": _, err = cfg.Getbool(k) case "INT": _, err = cfg.Getint(k) case "UINT": _, err = cfg.Getuint(k) case "FLOAT": _, err = cfg.Getfloat(k) case "STRING": _, err = cfg.Get(k) } if err != nil { t.Errorf("parser: %q got wrong value", k) } } if _, err = cfg.Get("no_key"); err != ErrKey { t.Error("expected to get ErrKey") } if err = cfg.Unmarshal(conf_ok); err != nil { t.Error(err) } if err = cfg.Unmarshal(conf_bad); err != ErrStructPtr { t.Error("expected to get ErrStructPtr") } if separator[0] != cfg.separator[0] { t.Errorf("separator: expected %q, got %q", separator, cfg.separator) } // == Editing if err = cfg.Set("STRING", "big"); err != nil { t.Fatal(err) } if cfg.data["STRING"] != "big" { t.Errorf("edit: value %q could not be set in key %q", "big", "STRING") } if err = cfg.Set("Not", ""); err == nil { t.Errorf("edit: key %q should not exist", "Not") } // == if testing.Verbose() { cfg.Print() } } osutil-1.5.0/distro/000077500000000000000000000000001462760513500143515ustar00rootroot00000000000000osutil-1.5.0/distro/distro.go000066400000000000000000000031111462760513500162000ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package distro detects the Linux distribution. package distro import ( "os" "github.com/tredoe/osutil/config/shconf" ) // Distro represents a distribution of Linux system. type Distro int // Most used Linux distributions. const ( DistroUnknown Distro = iota Arch CentOS Debian Fedora Manjaro OpenSUSE Ubuntu ) var distroNames = [...]string{ DistroUnknown: "unknown distribution", Arch: "Arch", CentOS: "CentOS", Debian: "Debian", Fedora: "Fedora", Manjaro: "Manjaro", OpenSUSE: "openSUSE", Ubuntu: "Ubuntu", } func (s Distro) String() string { return distroNames[s] } var idToDistro = map[string]Distro{ "arch": Arch, "manjaro": Manjaro, // based on Arch "centos": CentOS, "debian": Debian, "fedora": Fedora, "opensuse-leap": OpenSUSE, "opensuse-tumbleweed": OpenSUSE, "ubuntu": Ubuntu, } // Detect returns the Linux distribution. func Detect() (Distro, error) { var id string var err error if _, err = os.Stat("/etc/os-release"); !os.IsNotExist(err) { cfg, err := shconf.ParseFile("/etc/os-release") if err != nil { return 0, err } if id, err = cfg.Get("ID"); err != nil { return 0, err } if v, found := idToDistro[id]; found { return v, nil } } return DistroUnknown, nil } osutil-1.5.0/distro/distro_test.go000066400000000000000000000006031462760513500172420ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package distro import "testing" func TestDetect(t *testing.T) { if distro, err := Detect(); err != nil { t.Fatal(err) } else { t.Log(distro) } } osutil-1.5.0/file/000077500000000000000000000000001462760513500137645ustar00rootroot00000000000000osutil-1.5.0/file/a_test.go000066400000000000000000000005771462760513500156030ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "os" "path/filepath" ) var ( TEMP_FILE = filepath.Join(os.TempDir(), "test-file.txt") TEMP_BACKUP = TEMP_FILE + "+1~" ) osutil-1.5.0/file/doc.go000066400000000000000000000013651462760513500150650ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. /* Package file handles common operations in files. The editing of files is very important in the shell scripting to working with the configuration files. There are a great number of functions related to it, avoiding to have to use an external command to get the same result and with the advantage of that it creates automatically a backup before of editing. NewEdit creates a new struct, edit, which has a variable, CommentChar, with a value by default, '#'. That value is the character used in comments. */ package file osutil-1.5.0/file/edit.go000066400000000000000000000232251462760513500152440ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "bufio" "bytes" "io" "io/ioutil" "os" "regexp" ) // editDefault represents the vaues by default to set in type edit. type editDefault struct { CommentChar string // character used in comments //DoBackup bool // do backup before of edit? } // Values by default for type edit. var _editDefault = editDefault{"#"} // edit represents the file to edit. type edit struct { editDefault file *os.File buf *bufio.ReadWriter } type Replacer struct { Search, Replace string } type ReplacerAtLine struct { Line, Search, Replace string } // NewEdit opens a file to edit; it is created a backup. func NewEdit(filename string) (*edit, error) { if err := Backup(filename); err != nil { return nil, err } file, err := os.OpenFile(filename, os.O_RDWR, 0666) if err != nil { return nil, err } return &edit{ _editDefault, file, bufio.NewReadWriter(bufio.NewReader(file), bufio.NewWriter(file)), }, nil } // Append writes len(b) bytes at the end of the File. It returns an error, if any. func (e *edit) Append(b []byte) error { _, err := e.file.Seek(0, io.SeekEnd) if err != nil { return err } _, err = e.file.Write(b) return err } // AppendString is like Append, but writes the contents of string s rather than // an array of bytes. func (e *edit) AppendString(s string) error { return e.Append([]byte(s)) } // Close closes the file. func (e *edit) Close() error { return e.file.Close() } // Comment inserts the comment character in lines that mach any regular expression // in reLine. func (e *edit) Comment(reLine []string) error { allReSearch := make([]*regexp.Regexp, len(reLine)) for i, v := range reLine { if re, err := regexp.Compile(v); err != nil { return err } else { allReSearch[i] = re } } if _, err := e.file.Seek(0, io.SeekStart); err != nil { return err } char := []byte(e.CommentChar + " ") isNew := false buf := new(bytes.Buffer) // Check every line. for { line, err := e.buf.ReadBytes('\n') if err == io.EOF { break } for _, v := range allReSearch { if v.Match(line) { line = append(char, line...) if !isNew { isNew = true } break } } if _, err = buf.Write(line); err != nil { return err } } if isNew { return e.rewrite(buf.Bytes()) } return nil } // CommentOut removes the comment character of lines that mach any regular expression // in reLine. func (e *edit) CommentOut(reLine []string) error { allSearch := make([]ReplacerAtLine, len(reLine)) for i, v := range reLine { allSearch[i] = ReplacerAtLine{ v, "[[:space:]]*" + e.CommentChar + "[[:space:]]*", "", } } return e.ReplaceAtLineN(allSearch, 1) } /*// Insert writes len(b) bytes at the start of the File. It returns an error, if any. func (e *edit) Insert(b []byte) error { return e.rewrite(b) } // InsertString is like Insert, but writes the contents of string s rather than // an array of bytes. func (e *edit) InsertString(s string) error { return e.rewrite([]byte(s)) }*/ // Replace replaces all regular expressions mathed in r. func (e *edit) Replace(r []Replacer) error { return e.genReplace(r, -1) } // ReplaceN replaces regular expressions mathed in r. The count determines the // number to match: // n > 0: at most n matches // n == 0: the result is none // n < 0: all matches func (e *edit) ReplaceN(r []Replacer, n int) error { return e.genReplace(r, n) } // ReplaceAtLine replaces all regular expressions mathed in r, if the line is // matched at the first. func (e *edit) ReplaceAtLine(r []ReplacerAtLine) error { return e.genReplaceAtLine(r, -1) } // ReplaceAtLine replaces regular expressions mathed in r, if the line is // matched at the first. The count determines the // number to match: // n > 0: at most n matches // n == 0: the result is none // n < 0: all matches func (e *edit) ReplaceAtLineN(r []ReplacerAtLine, n int) error { return e.genReplaceAtLine(r, n) } // Generic Replace: replaces a number of regular expressions matched in r. func (e *edit) genReplace(r []Replacer, n int) error { if n == 0 { return nil } if _, err := e.file.Seek(0, io.SeekStart); err != nil { return err } content, err := ioutil.ReadAll(e.buf) if err != nil { return err } isNew := false for _, v := range r { reSearch, err := regexp.Compile(v.Search) if err != nil { return err } i := n repl := []byte(v.Replace) content = reSearch.ReplaceAllFunc(content, func(s []byte) []byte { if !isNew { isNew = true } if i != 0 { i-- return repl } return s }) } if isNew { return e.rewrite(content) } return nil } // Generic ReplaceAtLine: replaces a number of regular expressions matched in r, // if the line is matched at the first. func (e *edit) genReplaceAtLine(r []ReplacerAtLine, n int) error { if n == 0 { return nil } if _, err := e.file.Seek(0, io.SeekStart); err != nil { return err } // == Cache the regular expressions allReLine := make([]*regexp.Regexp, len(r)) allReSearch := make([]*regexp.Regexp, len(r)) allRepl := make([][]byte, len(r)) for i, v := range r { if reLine, err := regexp.Compile(v.Line); err != nil { return err } else { allReLine[i] = reLine } if reSearch, err := regexp.Compile(v.Search); err != nil { return err } else { allReSearch[i] = reSearch } allRepl[i] = []byte(v.Replace) } buf := new(bytes.Buffer) isNew := false // Replace every line, if it maches for { line, err := e.buf.ReadBytes('\n') if err == io.EOF { break } for i, _ := range r { if allReLine[i].Match(line) { j := n line = allReSearch[i].ReplaceAllFunc(line, func(s []byte) []byte { if !isNew { isNew = true } if j != 0 { j-- return allRepl[i] } return s }) } } if _, err = buf.Write(line); err != nil { return err } } if isNew { return e.rewrite(buf.Bytes()) } return nil } func (e *edit) rewrite(b []byte) error { if _, err := e.file.Seek(0, io.SeekStart); err != nil { return err } n, err := e.file.Write(b) if err != nil { return err } if err = e.file.Truncate(int64(n)); err != nil { return err } return nil // e.file.Sync() } // * * * // Append writes len(b) bytes at the end of the named file. It returns an // error, if any. The file is backed up. func Append(filename string, b []byte) error { e, err := NewEdit(filename) if err != nil { return err } err = e.Append(b) err2 := e.Close() if err != nil { return err } return err2 } // AppendString is like Append, but writes the contents of string s rather than // an array of bytes. func AppendString(filename, s string) error { return Append(filename, []byte(s)) } // Comment inserts the comment character in lines that mach the regular expression // in reLine, in the named file. func Comment(filename, reLine string) error { return CommentM(filename, []string{reLine}) } // CommentM inserts the comment character in lines that mach any regular expression // in reLine, in the named file. func CommentM(filename string, reLine []string) error { e, err := NewEdit(filename) if err != nil { return err } err = e.Comment(reLine) err2 := e.Close() if err != nil { return err } return err2 } // CommentOut removes the comment character of lines that mach the regular expression // in reLine, in the named file. func CommentOut(filename, reLine string) error { return CommentOutM(filename, []string{reLine}) } // CommentOutM removes the comment character of lines that mach any regular expression // in reLine, in the named file. func CommentOutM(filename string, reLine []string) error { e, err := NewEdit(filename) if err != nil { return err } err = e.CommentOut(reLine) err2 := e.Close() if err != nil { return err } return err2 } /*// Insert writes len(b) bytes at the start of the named file. It returns an // error, if any. The file is backed up. func Insert(filename string, b []byte) error { e, err := NewEdit(filename) if err != nil { return err } err = e.Insert(b) err2 := e.Close() if err != nil { return err } return err2 } // InsertString is like Insert, but writes the contents of string s rather than // an array of bytes. func InsertString(filename, s string) error { return Insert(filename, []byte(s)) }*/ // Replace replaces all regular expressions mathed in r for the named file. func Replace(filename string, r []Replacer) error { e, err := NewEdit(filename) if err != nil { return err } err = e.genReplace(r, -1) err2 := e.Close() if err != nil { return err } return err2 } // ReplaceN replaces a number of regular expressions mathed in r for the named // file. func ReplaceN(filename string, r []Replacer, n int) error { e, err := NewEdit(filename) if err != nil { return err } err = e.genReplace(r, n) err2 := e.Close() if err != nil { return err } return err2 } // ReplaceAtLine replaces all regular expressions mathed in r for the named // file, if the line is matched at the first. func ReplaceAtLine(filename string, r []ReplacerAtLine) error { e, err := NewEdit(filename) if err != nil { return err } err = e.genReplaceAtLine(r, -1) err2 := e.Close() if err != nil { return err } return err2 } // ReplaceAtLineN replaces a number of regular expressions mathed in r for the // named file, if the line is matched at the first. func ReplaceAtLineN(filename string, r []ReplacerAtLine, n int) error { e, err := NewEdit(filename) if err != nil { return err } err = e.genReplaceAtLine(r, n) err2 := e.Close() if err != nil { return err } return err2 } osutil-1.5.0/file/edit_test.go000066400000000000000000000073251462760513500163060ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "os" "testing" "github.com/tredoe/osutil/sh" ) func TestCreate(t *testing.T) { if err := CreateString(TEMP_FILE, ` Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. `); err != nil { t.Fatal(err) } out, err := sh.Runf("wc -l %s", TEMP_FILE) if err != nil { t.Fatal(err) } if out[0] != '7' { t.Fatalf("got %q lines, want 7", out[0]) } } func TestEdit(t *testing.T) { line := "I've heard that the night is all magic.\n" e, err := NewEdit(TEMP_FILE) if err != nil { t.Fatal(err) } /*defer func() { if err = os.Remove(TEMP_FILE); err != nil { t.Error(err) } }()*/ defer func() { if err = e.Close(); err != nil { t.Error(err) } }() // The backup should be created. if _, err = os.Stat(TEMP_BACKUP); err != nil { t.Error(err) } defer func() { if err = os.Remove(TEMP_BACKUP); err != nil { t.Error(err) } }() // Append if err = e.AppendString("\n" + line); err != nil { t.Error(err) } else { if out, _ := sh.Run("tail -n1 " + TEMP_FILE); string(out) != line { t.Errorf("Append => got %q, want %q", out, line) } } /*// Insert if err = e.InsertString(line); err != nil { t.Error(err) } else { if out, _, _ := sh.Run("head -n1 " + TEMP_FILE); out != line { t.Errorf("Insert => got %q, want %q", out, line) } }*/ // Replace repl := []Replacer{ {"dolor", "DOL_"}, {"labor", "LABOR_"}, } resul := "3\n" if err = e.Replace(repl); err != nil { t.Error(err) } else { if out, _ := sh.Runf("grep -c %s %s", repl[1].Replace, TEMP_FILE); string(out) != resul { t.Errorf("Replace (%s) => got %v, want %v", repl[1].Replace, out, resul) } } repl = []Replacer{ {"DOL_", "dOlOr"}, {"LABOR_", "lAbOr"}, } resul = "1\n" if err = e.ReplaceN(repl, 1); err != nil { t.Error(err) } else { for i := 0; i <= 1; i++ { if out, _ := sh.Runf("grep -c %s %s", repl[i].Replace, TEMP_FILE); string(out) != resul { t.Errorf("Replace (%s) => got %v, want %v", repl[i].Replace, out, resul) } } } // ReplaceAtLine replAt := []ReplacerAtLine{ {"LABOR", "o", "OO"}, } resul = "2\n" if err = e.ReplaceAtLine(replAt); err != nil { t.Error(err) } else { if out, _ := sh.Run("grep -c OO " + TEMP_FILE); string(out) != resul { t.Errorf("ReplaceAtLine => got %v, want %v", out, resul) } } replAt = []ReplacerAtLine{ {"heard", "a", "AA"}, } resul = "1\n" if err = e.ReplaceAtLineN(replAt, 2); err != nil { t.Error(err) } else { if out, _ := sh.Runf("tail -n1 %s | grep -c A", TEMP_FILE); string(out) != resul { t.Errorf("ReplaceAtLineN => got %v, want %v", out, resul) } } // Comment resul = "2\n" if err = e.Comment([]string{"night", "quis"}); err != nil { t.Error(err) } else { if out, _ := sh.Runf("grep -c %s %s", e.CommentChar, TEMP_FILE); string(out) != resul { t.Errorf("Comment => got %v, want %v", out, resul) } } // CommentOut resul = "0\n" if err = e.CommentOut([]string{"night", "quis"}); err != nil { t.Error(err) } else { if out, _ := sh.Runf("grep -c %s %s", e.CommentChar, TEMP_FILE); string(out) != resul { t.Errorf("CommentOut => got %v, want %v", out, resul) } } } osutil-1.5.0/file/find.go000066400000000000000000000023161462760513500152350ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "bufio" "bytes" "fmt" "io" "os" "strings" ) // Contain returns whether the named file contains the byte slice b. The // return value is a boolean. func Contain(filename string, b []byte) (bool, error) { f, err := os.Open(filename) if err != nil { return false, fmt.Errorf("Contain: %s", err) } defer f.Close() buf := bufio.NewReader(f) for { line, err := buf.ReadBytes('\n') if err == io.EOF { break } if bytes.Contains(line, b) { return true, nil } } return false, nil } // ContainString returns whether the named file contains the string s. The // return value is a boolean. func ContainString(filename, s string) (bool, error) { f, err := os.Open(filename) if err != nil { return false, fmt.Errorf("ContainString: %s", err) } defer f.Close() buf := bufio.NewReader(f) for { line, err := buf.ReadString('\n') if err == io.EOF { break } if strings.Contains(line, s) { return true, nil } } return false, nil } osutil-1.5.0/file/find_test.go000066400000000000000000000011311462760513500162660ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import "testing" func TestFind(t *testing.T) { ok, err := ContainString(TEMP_FILE, "night") if err != nil { t.Fatal(err) } if !ok { t.Errorf("ContainString: expected %t, found %t", !ok, ok) } ok, err = Contain(TEMP_FILE, []byte("night")) if err != nil { t.Fatal(err) } if !ok { t.Errorf("Contain: expected %t, found %t", !ok, ok) } } osutil-1.5.0/file/info.go000066400000000000000000000070421462760513500152510ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import "os" // flags got in: `man 2 stat` const ( modeROwner = 00400 // owner has read permission modeWOwner = 00200 // owner has write permission modeXOwner = 00100 // owner has execute permission modeRGroup = 00040 // group has read permission modeWGroup = 00020 // group has write permission modeXGroup = 00010 // group has execute permission modeROthers = 00004 // others have read permission modeWOthers = 00002 // others have write permission modeXOthers = 00001 // others have execute permission ) type perm uint8 // permissions const ( _ perm = iota R // read W // write X // execute ) // info represents a wrapper about os.FileInfo to append some functions. type info struct{ fi os.FileInfo } // NewInfo returns a info describing the named file. func NewInfo(name string) (*info, error) { i, err := os.Stat(name) if err != nil { return nil, err } return &info{i}, nil } // IsDir reports whether if it is a directory. func (i *info) IsDir() bool { return i.fi.IsDir() } // IsFile reports whether it is a regular file. func (i *info) IsFile() bool { return i.fi.Mode()&os.ModeType == 0 } // OwnerHas reports whether the owner has all given permissions. func (i *info) OwnerHas(p ...perm) bool { mode := i.fi.Mode() for _, v := range p { switch v { case R: if mode&modeROwner == 0 { return false } case W: if mode&modeWOwner == 0 { return false } case X: if mode&modeXOwner == 0 { return false } } } return true } // GroupHas reports whether the group has all given permissions. func (i *info) GroupHas(p ...perm) bool { mode := i.fi.Mode() for _, v := range p { switch v { case R: if mode&modeRGroup == 0 { return false } case W: if mode&modeWGroup == 0 { return false } case X: if mode&modeXGroup == 0 { return false } } } return true } // OthersHave reports whether the others have all given permissions. func (i *info) OthersHave(p ...perm) bool { mode := i.fi.Mode() for _, v := range p { switch v { case R: if mode&modeROthers == 0 { return false } case W: if mode&modeWOthers == 0 { return false } case X: if mode&modeXOthers == 0 { return false } } } return true } // * * * // IsDir reports whether if the named file is a directory. func IsDir(name string) (bool, error) { i, err := NewInfo(name) if err != nil { return false, err } return i.IsDir(), nil } // IsFile reports whether the named file is a regular file. func IsFile(name string) (bool, error) { i, err := NewInfo(name) if err != nil { return false, err } return i.IsFile(), nil } // OwnerHas reports whether the named file has all given permissions for the owner. func OwnerHas(name string, p ...perm) (bool, error) { i, err := NewInfo(name) if err != nil { return false, err } return i.OwnerHas(p...), nil } // GroupHas reports whether the named file has all given permissions for the group. func GroupHas(name string, p ...perm) (bool, error) { i, err := NewInfo(name) if err != nil { return false, err } return i.GroupHas(p...), nil } // OthersHave reports whether the named file have all given permissions for the others. func OthersHave(name string, p ...perm) (bool, error) { i, err := NewInfo(name) if err != nil { return false, err } return i.OthersHave(p...), nil } osutil-1.5.0/file/info_test.go000066400000000000000000000011331462760513500163030ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "testing" ) func TestInfo(t *testing.T) { ok, err := IsDir("../file") if err != nil { t.Error(err) } else if !ok { t.Error("IsDir got false") } fi, err := NewInfo("info.go") if err != nil { t.Fatal(err) } if !fi.OwnerHas(R, W) { t.Error("OwnerHas(R,W) got false") } if fi.OwnerHas(X) { t.Error("OwnerHas(X) got true") } } osutil-1.5.0/file/io.go000066400000000000000000000076341462760513500147340ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "fmt" "io" "io/ioutil" "os" "path/filepath" ) // Copy copies file in source to file in dest preserving the mode attributes. func Copy(source, dest string) (err error) { // Don't backup files of backup. if dest[len(dest)-1] != '~' { if err = Backup(dest); err != nil { return } } srcFile, err := os.Open(source) if err != nil { return err } defer func() { err2 := srcFile.Close() if err2 != nil && err == nil { err = err2 } }() srcInfo, err := os.Stat(source) if err != nil { return err } dstFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode().Perm()) if err != nil { return err } _, err = io.Copy(dstFile, srcFile) err2 := dstFile.Close() if err2 != nil && err == nil { err = err2 } return } // Create creates a new file with b bytes. func Create(filename string, b []byte) (err error) { file, err := os.Create(filename) if err != nil { return err } _, err = file.Write(b) err2 := file.Close() if err2 != nil && err == nil { err = err2 } return } // CreateString is like Create, but writes the contents of string s rather than // an array of bytes. func CreateString(filename, s string) error { return Create(filename, []byte(s)) } // Overwrite truncates the named file to zero and writes len(b) bytes. It // returns an error, if any. func Overwrite(filename string, b []byte) (err error) { if err := Backup(filename); err != nil { return err } file, err := os.Create(filename) if err != nil { return err } _, err = file.Write(b) err2 := file.Close() if err2 != nil && err == nil { err = err2 } return } // OverwriteString is like Overwrite, but writes the contents of string s rather // than an array of bytes. func OverwriteString(filename, s string) error { return Overwrite(filename, []byte(s)) } // == Utility const _BACKUP_SUFFIX = "+[1-9]~" // Suffix pattern added to backup's file name. const PREFIX_TEMP = "test-" // Prefix to add to temporary files. // Backup creates a backup of the named file. // // The schema used for the new name is: {name}\+[1-9]~ // name: The original file name. // + : Character used to separate the file name from rest. // number: A number from 1 to 9, using rotation. // ~ : To indicate that it is a backup, just like it is used in Unix systems. func Backup(filename string) error { // Check if it is empty info, err := os.Stat(filename) if err != nil { if os.IsNotExist(err) { return nil } return err } if info.Size() == 0 { return nil } files, err := filepath.Glob(filename + _BACKUP_SUFFIX) if err != nil { return err } // Number rotation numBackup := byte(1) if len(files) != 0 { lastFile := files[len(files)-1] numBackup = lastFile[len(lastFile)-2] + 1 // next number if numBackup > '9' { numBackup = '1' } } else { numBackup = '1' } return Copy(filename, fmt.Sprintf("%s+%s~", filename, string(numBackup))) } // CopytoTemp creates a temporary file from the source file into the default // directory for temporary files (see os.TempDir), whose name begins with prefix. // If prefix is the empty string, uses the default value PREFIX_TEMP. // Returns the temporary file name. func CopytoTemp(source, prefix string) (tmpFile string, err error) { if prefix == "" { prefix = PREFIX_TEMP } src, err := os.Open(source) if err != nil { return "", err } defer func() { err2 := src.Close() if err2 != nil && err == nil { err = err2 } }() dest, err := ioutil.TempFile("", prefix) if err != nil { return "", err } defer func() { err2 := dest.Close() if err2 != nil && err == nil { err = err2 } }() if _, err = io.Copy(dest, src); err != nil { return "", err } return dest.Name(), nil } osutil-1.5.0/file/io_test.go000066400000000000000000000026261462760513500157670ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "os" "path/filepath" "strings" "testing" ) func TestBackupSuffix(t *testing.T) { okFilenames := []string{"foo+1~", "foo+2~", "foo+5~", "foo+8~", "foo+9~"} badFilenames := []string{"foo+0~", "foo+10~", "foo+11~", "foo+22~"} for _, v := range okFilenames { ok, err := filepath.Match("foo"+_BACKUP_SUFFIX, v) if err != nil { t.Fatal(err) } if !ok { t.Errorf("expected to not match %q", v) } } for _, v := range badFilenames { ok, err := filepath.Match("foo"+_BACKUP_SUFFIX, v) if err != nil { t.Fatal(err) } if ok { t.Errorf("expected to not match %q", v) } } } const FILENAME = "doc.go" func TestCopytoTemp(t *testing.T) { name, err := CopytoTemp(FILENAME, "") if err != nil { t.Fatal(err) } checkCopytoTemp(name, PREFIX_TEMP, t) name, err = CopytoTemp(FILENAME, "foo-") if err != nil { t.Fatal(err) } checkCopytoTemp(name, "foo-", t) } func checkCopytoTemp(filename, prefix string, t *testing.T) { if prefix == "" { prefix = PREFIX_TEMP } if !strings.HasPrefix(filename, filepath.Join(os.TempDir(), prefix)) { t.Error("got wrong prefix") } if err := os.Remove(filename); err != nil { t.Error(err) } } osutil-1.5.0/file/z_test.go000066400000000000000000000005621462760513500156260ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package file import ( "os" "testing" ) func Test_z(t *testing.T) { if err := os.Remove(TEMP_FILE); err != nil { t.Error(err) } } osutil-1.5.0/go.mod000066400000000000000000000004451462760513500141560ustar00rootroot00000000000000module github.com/tredoe/osutil go 1.16 // Breaking changes // curl proxy.golang.org/github.com/tredoe/osutil/@v/list |sort -V retract ( v1.1.0 v1.1.1 v1.1.2 v1.1.3 v1.1.4 v1.1.5 v1.1.6 v1.1.7 v1.1.8 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.3.2 v1.3.3 v1.3.4 v1.3.5 v1.3.6 v2.0.0 ) osutil-1.5.0/internal/000077500000000000000000000000001462760513500146615ustar00rootroot00000000000000osutil-1.5.0/internal/reflect.go000066400000000000000000000024711462760513500166400ustar00rootroot00000000000000// Copyright 2014 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Implements some reflection utility functions. package internal import ( "fmt" "reflect" "runtime" "strconv" ) // GetFunctionName returns the name of a function. func GetFunctionName(i interface{}) string { return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() } // PrintStruct prints the field names and values of the given struct. // It is used to debug. func PrintStruct(v interface{}) { valueof := reflect.ValueOf(v).Elem() typeof := valueof.Type() var value interface{} for i := 0; i < valueof.NumField(); i++ { fieldT := typeof.Field(i) fieldV := valueof.Field(i) switch fieldV.Kind() { case reflect.Bool: value = fieldV.Bool() case reflect.Int: value = strconv.Itoa(int(fieldV.Int())) case reflect.Slice: //value = fieldV.Slice(0, fieldV.Len()) /*for j := 0; j < fieldV.NumField(); j++ { fmt.Println(fieldV.Index[j]) }*/ //fmt.Println(fieldV.Elem()) fallthrough case reflect.String: value = fieldV.String() default: panic(fieldV.Kind().String() + ": type not added") } fmt.Printf(" %s: %v\n", fieldT.Name, value) } } osutil-1.5.0/osutil.go000066400000000000000000000022361462760513500147160ustar00rootroot00000000000000// Copyright 2014 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package osutil import ( "errors" "os" "os/exec" ) // Exec executes a command setting both standard input, output and error. func Exec(cmd string, args ...string) error { c := exec.Command(cmd, args...) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr if err := c.Run(); err != nil { return err } return nil } // ExecSudo executes a command under "sudo". func ExecSudo(cmd string, args ...string) error { return Exec("sudo", append([]string{cmd}, args...)...) } // Sudo executes command "sudo". // If some command needs to use "sudo", then could be used this function at // the beginning so there is not to wait until that it been requested later. func Sudo() error { return Exec("sudo", "/bin/true") } var ErrNoRoot = errors.New("MUST have administrator privileges") // MustbeRoot returns an error message if the user is not root. func MustbeRoot() error { if os.Getuid() != 0 { return ErrNoRoot } return nil } osutil-1.5.0/osutil_test.go000066400000000000000000000005421462760513500157530ustar00rootroot00000000000000// Copyright 2014 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package osutil import ( "testing" ) func TestSudo(t *testing.T) { if err := Sudo(); err != nil { t.Error(err) } } osutil-1.5.0/pkgutil/000077500000000000000000000000001462760513500145245ustar00rootroot00000000000000osutil-1.5.0/pkgutil/Info.md000066400000000000000000000014711462760513500157440ustar00rootroot00000000000000 ## Linux #### Deb #### RPM http://fedoraproject.org/wiki/FAQ#How_do_I_install_new_software_on_Fedora.3F_Is_there_anything_like_APT.3F http://yum.baseurl.org/wiki/YumCommands #### Pacman https://wiki.archlinux.org/index.php/Pacman#Usage http://www.archlinux.org/pacman/pacman.8.html "--noconfirm" bypasses the "Are you sure?" checks #### Ebuild http://www.gentoo.org/doc/en/handbook/handbook-x86.xml?part=2&chap=1 http://www.gentoo-wiki.info/MAN_emerge "--ask" argument #### ZYpp http://en.opensuse.org/SDB:Zypper_usage http://www.openss7.org/man2html?zypper%288%29 "--no-confirm" ## Windows http://www.howtogeek.com/200334/windows-10-includes-a-linux-style-package-manager-named-oneget/?PageSpeed=noscript http://blogs.msdn.com/b/garretts/archive/2014/04/01/my-little-secret-windows-powershell-oneget.aspx osutil-1.5.0/pkgutil/deb.go000066400000000000000000000021451462760513500156070ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "github.com/tredoe/osutil" type deb struct{} func (p deb) Install(name ...string) error { args := []string{"install", "-y"} return osutil.ExecSudo("/usr/bin/apt-get", append(args, name...)...) } func (p deb) Remove(name ...string) error { args := []string{"remove", "-y"} return osutil.ExecSudo("/usr/bin/apt-get", append(args, name...)...) } func (p deb) Purge(name ...string) error { args := []string{"purge", "-y"} return osutil.ExecSudo("/usr/bin/apt-get", append(args, name...)...) } func (p deb) Update() error { return osutil.ExecSudo("/usr/bin/apt-get", "update", "-qq") } func (p deb) Upgrade() error { return osutil.ExecSudo("/usr/bin/apt-get", "upgrade") } func (p deb) Clean() error { err := osutil.ExecSudo("/usr/bin/apt-get", "autoremove", "-y") if err != nil { return err } return osutil.ExecSudo("/usr/bin/apt-get", "clean") } osutil-1.5.0/pkgutil/ebuild.go000066400000000000000000000020311462760513500163130ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "github.com/tredoe/osutil" type ebuild struct{} func (p ebuild) Install(name ...string) error { return osutil.Exec("/usr/bin/emerge", name...) } func (p ebuild) Remove(name ...string) error { args := []string{"--unmerge"} return osutil.Exec("/usr/bin/emerge", append(args, name...)...) } func (p ebuild) Purge(name ...string) error { return p.Remove(name...) } func (p ebuild) Update() error { return osutil.Exec("/usr/bin/emerge", "--sync") } func (p ebuild) Upgrade() error { return osutil.Exec("/usr/bin/emerge", "--update", "--deep", "--with-bdeps=y", "--newuse @world") } func (p ebuild) Clean() error { err := osutil.Exec("/usr/bin/emerge", "--update", "--deep", "--newuse @world") if err != nil { return err } return osutil.Exec("/usr/bin/emerge", "--depclean") } osutil-1.5.0/pkgutil/pacman.go000066400000000000000000000020201462760513500163040ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "github.com/tredoe/osutil" type pacman struct{} func (p pacman) Install(name ...string) error { args := []string{"-S", "--needed", "--noprogressbar"} return osutil.Exec("/usr/bin/pacman", append(args, name...)...) } func (p pacman) Remove(name ...string) error { args := []string{"-Rs"} return osutil.Exec("/usr/bin/pacman", append(args, name...)...) } func (p pacman) Purge(name ...string) error { args := []string{"-Rsn"} return osutil.Exec("/usr/bin/pacman", append(args, name...)...) } func (p pacman) Update() error { return osutil.Exec("/usr/bin/pacman", "-Syu", "--needed", "--noprogressbar") } func (p pacman) Upgrade() error { return osutil.Exec("/usr/bin/pacman", "-Syu") } func (p pacman) Clean() error { return osutil.Exec("/usr/bin/paccache", "-r") } osutil-1.5.0/pkgutil/pkg.go000066400000000000000000000045371462760513500156450ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package pkgutil handles basic operations in the management of packages in // operating systems. // // Important // // If you are going to use a package manager different to Deb, then you should // check the options since I cann't test all. // // TODO // // Add managers of BSD systems. // // Use flag to do not show questions. package pkgutil import ( "errors" "os/exec" ) // Packager is the common interface to handle different package systems. type Packager interface { // Install installs packages. Install(name ...string) error // Remove removes packages. Remove(name ...string) error // Purge removes packages and its configuration files. Purge(name ...string) error // Update resynchronizes the package index files from their sources. Update() error // Upgrade upgrades all the packages on the system. Upgrade() error // Clean erases both packages downloaded and orphaned dependencies. Clean() error } // PackageType represents a package management system. type PackageType int8 const ( // Linux Deb PackageType = iota + 1 RPM Pacman Ebuild ZYpp ) func (pkg PackageType) String() string { switch pkg { case Deb: return "Deb" case RPM: return "RPM" case Pacman: return "Pacman" case Ebuild: return "Ebuild" case ZYpp: return "ZYpp" } panic("unreachable") } // New returns the interface to handle the package manager. func New(pkg PackageType) Packager { switch pkg { case Deb: return new(deb) case RPM: return new(rpm) case Pacman: return new(pacman) case Ebuild: return new(ebuild) case ZYpp: return new(zypp) } panic("unreachable") } // execPackagers is a list of executables of package managers. var execPackagers = [...]string{ Deb: "apt-get", RPM: "yum", Pacman: "pacman", Ebuild: "emerge", ZYpp: "zypper", } // Detect tries to get the package system used in the system, looking for // executables in directory "/usr/bin". func Detect() (PackageType, error) { for k, v := range execPackagers { _, err := exec.LookPath("/usr/bin/" + v) if err == nil { return PackageType(k), nil } } return -1, errors.New("package manager not found in directory '/usr/bin'") } osutil-1.5.0/pkgutil/pkg_test.go000066400000000000000000000013611462760513500166740ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "testing" func TestPackager(t *testing.T) { pkg, err := Detect() if err != nil { t.Fatal(err) } pack := New(pkg) cmd := "mtr-tiny" if !testing.Verbose() { return } if err = pack.Update(); err != nil { t.Fatal(err) } if err = pack.Upgrade(); err != nil { t.Fatal(err) } if err = pack.Install(cmd); err != nil { t.Errorf("\n%s", err) } if err = pack.Remove(cmd); err != nil { t.Errorf("\n%s", err) } if err = pack.Clean(); err != nil { t.Errorf("\n%s", err) } } osutil-1.5.0/pkgutil/rpm.go000066400000000000000000000016051462760513500156530ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "github.com/tredoe/osutil" type rpm struct{} func (p rpm) Install(name ...string) error { args := []string{"install"} return osutil.Exec("/usr/bin/yum", append(args, name...)...) } func (p rpm) Remove(name ...string) error { args := []string{"remove"} return osutil.Exec("/usr/bin/yum", append(args, name...)...) } func (p rpm) Purge(name ...string) error { return p.Remove(name...) } func (p rpm) Update() error { return osutil.Exec("/usr/bin/yum", "update") } func (p rpm) Upgrade() error { return osutil.Exec("/usr/bin/yum", "update") } func (p rpm) Clean() error { return osutil.Exec("/usr/bin/yum", "clean", "packages") } osutil-1.5.0/pkgutil/zypp.go000066400000000000000000000017101462760513500160540ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package pkgutil import "github.com/tredoe/osutil" type zypp struct{} func (p zypp) Install(name ...string) error { args := []string{"install", "--auto-agree-with-licenses"} return osutil.Exec("/usr/bin/zypper", append(args, name...)...) } func (p zypp) Remove(name ...string) error { args := []string{"remove"} return osutil.Exec("/usr/bin/zypper", append(args, name...)...) } func (p zypp) Purge(name ...string) error { return p.Remove(name...) } func (p zypp) Update() error { return osutil.Exec("/usr/bin/zypper", "refresh") } func (p zypp) Upgrade() error { return osutil.Exec("/usr/bin/zypper", "up", "--auto-agree-with-licenses") } func (p zypp) Clean() error { return osutil.Exec("/usr/bin/zypper", "clean") } osutil-1.5.0/sh/000077500000000000000000000000001462760513500134575ustar00rootroot00000000000000osutil-1.5.0/sh/log.go000066400000000000000000000026271462760513500145760ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package sh import ( "io/ioutil" "log" "log/syslog" "os" ) const PATH = "/sbin:/bin:/usr/sbin:/usr/bin" const logFilename = "/.shutil.log" // in boot var ( env []string home string // to expand symbol "~" BOOT bool // does the script is being run during boot? DEBUG bool logFile *os.File Log = log.New(ioutil.Discard, "", 0) ) // Sets environment variables and a null logger. func init() { log.SetFlags(0) log.SetPrefix("ERROR: ") if BOOT { env = []string{"PATH=" + PATH} // from file boot } else { env = os.Environ() home = os.Getenv("HOME") } /*if path := os.Getenv("PATH"); path == "" { if err = os.Setenv("PATH", PATH); err != nil { log.Print(err) } }*/ } // StartLogger initializes the log file. func StartLogger() { var err error if BOOT { if logFile, err = os.OpenFile(logFilename, os.O_WRONLY|os.O_TRUNC, 0); err != nil { log.Print(err) } else { Log = log.New(logFile, "", log.Lshortfile) } } else { if Log, err = syslog.NewLogger(syslog.LOG_NOTICE, log.Lshortfile); err != nil { log.Fatal(err) } } } // CloseLogger closes the log file. func CloseLogger() error { if BOOT { return logFile.Close() } return nil } osutil-1.5.0/sh/sh.go000066400000000000000000000164041462760513500144250ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Package sh interprets a command line just like it is done in the Bash shell. // // The main function is Run which lets to call to system commands under a new // process. It handles pipes, environment variables, and does pattern expansion. package sh import ( "bytes" "errors" "fmt" "io" "os" "os/exec" "path" "path/filepath" "strings" ) // Debug shows debug messages in functions like Run. var Debug bool // == Errors var ( errEnvVar = errors.New("the format of the variable has to be VAR=value") errNoCmdInPipe = errors.New("no command around of pipe") ) type extraCmdError string func (e extraCmdError) Error() string { return "command not added to " + string(e) } type runError struct { cmd string debug string errType string err error } func (e runError) Error() string { if Debug { if e.debug != "" { e.debug = "\n## DEBUG\n" + e.debug + "\n" } return fmt.Sprintf("Command line: `%s`\n%s\n## %s\n%s", e.cmd, e.debug, e.errType, e.err) } return fmt.Sprintf("\n%s", e.err) } // RunWithMatch executes external commands with access to shell features such as // filename wildcards, shell pipes, environment variables, and expansion of the // shortcut character "~" to home directory. // // This function avoids to have execute commands through a shell since an // unsanitized input from an untrusted source makes a program vulnerable to // shell injection, a serious security flaw which can result in arbitrary // command execution. // // The most of commands return a text in output or an error if any. // `match` is used in commands like *grep*, *find*, or *cmp* to indicate if the // serach is matched. func RunWithMatch(command string) (output []byte, match bool, err error) { var ( cmds []*exec.Cmd outPipes []io.ReadCloser stdout, stderr bytes.Buffer ) commands := strings.Split(command, "|") lastIdxCmd := len(commands) - 1 // Check lonely pipes. for _, cmd := range commands { if strings.TrimSpace(cmd) == "" { err = runError{command, "", "ERR", errNoCmdInPipe} return } } for i, cmd := range commands { cmdEnv := env // evironment variables for each command indexArgs := 1 // position where the arguments start fields := strings.Fields(cmd) lastIdxFields := len(fields) - 1 // == Get environment variables in the first arguments, if any. for j, fCmd := range fields { if fCmd[len(fCmd)-1] == '=' || // VAR= foo (j < lastIdxFields && fields[j+1][0] == '=') { // VAR =foo err = runError{command, "", "ERR", errEnvVar} return } if strings.ContainsRune(fields[0], '=') { cmdEnv = append([]string{fields[0]}, env...) // Insert the environment variable fields = fields[1:] // and it is removed from arguments } else { break } } // == cmdPath, e := exec.LookPath(fields[0]) if e != nil { err = runError{command, "", "ERR", e} return } // == Get the path of the next command, if any for j, fCmd := range fields { cmdBase := path.Base(fCmd) if cmdBase != "sudo" && cmdBase != "xargs" { break } // It should have an extra command. if j+1 == len(fields) { err = runError{command, "", "ERR", extraCmdError(cmdBase)} return } nextCmdPath, e := exec.LookPath(fields[j+1]) if e != nil { err = runError{command, "", "ERR", e} return } if fields[j+1] != nextCmdPath { fields[j+1] = nextCmdPath indexArgs = j + 2 } } // == Expansion of arguments expand := make(map[int][]string, len(fields)) for j := indexArgs; j < len(fields); j++ { // Skip flags if fields[j][0] == '-' { continue } // Shortcut character "~" if fields[j] == "~" || strings.HasPrefix(fields[j], "~/") { fields[j] = strings.Replace(fields[j], "~", home, 1) } // File name wildcards names, e := filepath.Glob(fields[j]) if e != nil { err = runError{command, "", "ERR", e} return } if names != nil { expand[j] = names } } // Substitute the names generated for the pattern starting from last field. if len(expand) != 0 { for j := len(fields) - indexArgs; j >= indexArgs; j-- { if v, ok := expand[j]; ok { fields = append(fields[:j], append(v, fields[j+1:]...)...) } } } // == Handle arguments with quotes hasQuote := false needUpdate := false tmpFields := []string{} for j := indexArgs; j < len(fields); j++ { v := fields[j] lastChar := v[len(v)-1] if !hasQuote && (v[0] == '\'' || v[0] == '"') { if !needUpdate { needUpdate = true } v = v[1:] // skip quote if lastChar == '\'' || lastChar == '"' { v = v[:len(v)-1] // remove quote } else { hasQuote = true } tmpFields = append(tmpFields, v) continue } if hasQuote { if lastChar == '\'' || lastChar == '"' { v = v[:len(v)-1] // remove quote hasQuote = false } tmpFields[len(tmpFields)-1] += " " + v continue } tmpFields = append(tmpFields, v) } if needUpdate { fields = append(fields[:indexArgs], tmpFields...) } // == Create command c := &exec.Cmd{ Path: cmdPath, Args: append([]string{fields[0]}, fields[1:]...), Env: cmdEnv, } // == Connect pipes outPipe, e := c.StdoutPipe() if e != nil { err = runError{command, "", "ERR", e} return } if i == 0 { c.Stdin = os.Stdin } else { c.Stdin = outPipes[i-1] // anterior output } // == Buffers c.Stderr = &stderr // Only save the last output if i == lastIdxCmd { c.Stdout = &stdout } // == Start command if e := c.Start(); e != nil { err = runError{command, fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args), "Start", fmt.Errorf("%s", c.Stderr)} return } // cmds = append(cmds, c) outPipes = append(outPipes, outPipe) } for _, c := range cmds { if e := c.Wait(); e != nil { _, isExitError := e.(*exec.ExitError) // Error type due I/O problems. if !isExitError { err = runError{command, fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args), "Wait", fmt.Errorf("%s", c.Stderr)} return } if c.Stderr != nil { if stderr := fmt.Sprintf("%s", c.Stderr); stderr != "" { stderr = strings.TrimRight(stderr, "\n") err = runError{command, fmt.Sprintf("- Command: %s\n- Args: %s", c.Path, c.Args), "Stderr", fmt.Errorf("%s", stderr)} return } } } else { match = true } } Log.Print(command) return stdout.Bytes(), match, nil } // Run executes external commands just like RunWithMatch, but does not return // the boolean `match`. func Run(command string) (output []byte, err error) { output, _, err = RunWithMatch(command) return } // Runf is like Run, but formats its arguments according to the format. // Analogous to Printf(). func Runf(format string, args ...interface{}) ([]byte, error) { return Run(fmt.Sprintf(format, args...)) } // RunWithMatchf is like RunWithMatch, but formats its arguments according to // the format. Analogous to Printf(). func RunWithMatchf(format string, args ...interface{}) ([]byte, bool, error) { return RunWithMatch(fmt.Sprintf(format, args...)) } osutil-1.5.0/sh/sh_test.go000066400000000000000000000036511462760513500154640ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package sh import ( "errors" "testing" ) var testsOk = []struct { cmd string match bool }{ // expansion of "~" {"ls ~/", true}, } var testsOutput = []struct { cmd string out string match bool }{ // values in match {"true", "", true}, {"false", "", false}, {`grep foo not_exist.go`, "", false}, // no found {`grep package sh.go`, "package sh\n", true}, // found // pipes {"ls sh*.go | wc -l", "2\n", true}, // quotes {`sh -c 'echo 123'`, "123\n", true}, {`sh -c "echo 123"`, "123\n", true}, {`find -name 'sh*.go'`, "./sh.go\n./sh_test.go\n", true}, } var testsError = []struct { cmd string err error // from Stderr }{ {"| ls ", errNoCmdInPipe}, {"| ls | wc", errNoCmdInPipe}, {"ls|", errNoCmdInPipe}, {"ls| wc|", errNoCmdInPipe}, {"ls| |wc", errNoCmdInPipe}, {"LANG= C find", errEnvVar}, {"LANG =C find", errEnvVar}, {`LANG=C find -nop README.md`, errors.New("find: unknown predicate `-nop'")}, } func TestRun(t *testing.T) { for _, v := range testsOk { out, match, _ := RunWithMatch(v.cmd) if v.match != match { t.Errorf("`%s` (match): expected %t, found %t\n", v.cmd, v.match, match) } if string(out) == "" { t.Errorf("`%s`: output is empty", v.cmd) } } for _, v := range testsOutput { out, match, _ := RunWithMatch(v.cmd) if string(out) != v.out { t.Errorf("`%s` (output): expected %q, found %q\n", v.cmd, v.out, out) } if match != v.match { t.Errorf("`%s` (match): expected %t, found %t\n", v.cmd, v.match, match) } } for _, v := range testsError { _, err := Run(v.cmd) mainErr := err.(runError).err if mainErr.Error() != v.err.Error() { t.Errorf("`%s` (error): expected %q, found %q\n", v.cmd, v.err, mainErr) } } } osutil-1.5.0/user/000077500000000000000000000000001462760513500140235ustar00rootroot00000000000000osutil-1.5.0/user/README.md000066400000000000000000000025131462760513500153030ustar00rootroot00000000000000# user Provides access to the users database. It is available for Linux (by now). [Documentation online](http://gowalker.org/github.com/tredoe/osutil/user) ## Installation go get github.com/tredoe/osutil/user To run the tests, it is necessary to run them as root. Do not worry because the tests are done in copies of original files. sudo env PATH=$PATH go test -v ## Status BSD systems and Windows unsopported. The only backend built is to handle files (such as '/etc/passwd'), and it it not my priority to handle other backends like LDAP or Kerberos since my goal was to can use it in home systems. My list of priorities are (for when I have time): + BSD systems (included Mac OS) + Windows ## Configuration Some values are got from the system configuration, i.e. to get the next available UID or GID, but every distribution of a same system can have a different configuration system. In the case of Linux, the research has been done in 10 different distributions: Arch CentOS Debian Fedora Gentoo Mageia (Mandriva's fork) OpenSUSE PCLinuxOS Slackware Ubuntu ## License The source files are distributed under the [Mozilla Public License, version 2.0](http://mozilla.org/MPL/2.0/), unless otherwise noted. Please read the [FAQ](http://www.mozilla.org/MPL/2.0/FAQ.html) if you have further questions regarding the license. osutil-1.5.0/user/a_test.go000066400000000000000000000025221462760513500156320ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "log" "os" "path/filepath" "github.com/tredoe/osutil" "github.com/tredoe/osutil/file" ) const ( USER = "u_foo" USER2 = "u_foo2" SYS_USER = "usys_bar" GROUP = "g_foo" SYS_GROUP = "gsys_bar" ) var MEMBERS = []string{USER, SYS_USER} // Stores the ids at creating the groups. var GID, SYS_GID int // == Copy the system files before of be edited. func init() { err := osutil.MustbeRoot() if err != nil { log.Fatalf("%s", err) } if fileUser, err = file.CopytoTemp(fileUser, "test-user_"); err != nil { goto _error } if fileGroup, err = file.CopytoTemp(fileGroup, "test-group_"); err != nil { goto _error } if fileShadow, err = file.CopytoTemp(fileShadow, "test-shadow_"); err != nil { goto _error } if fileGShadow, err = file.CopytoTemp(fileGShadow, "test-gshadow_"); err != nil { goto _error } return _error: removeTempFiles() log.Fatalf("%s", err) } func removeTempFiles() { files, _ := filepath.Glob(filepath.Join(os.TempDir(), file.PREFIX_TEMP+"*")) for _, f := range files { if err := os.Remove(f); err != nil { log.Printf("%s", err) } } } osutil-1.5.0/user/config_linux.go000066400000000000000000000143511462760513500170420ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "fmt" "strings" "sync" "github.com/tredoe/osutil/config/shconf" "github.com/tredoe/osutil/internal" "github.com/tredoe/osutil/user/crypt" ) // TODO: handle des, bcrypt and rounds in SHA2. // TODO: Idea: store struct "configData" to run configData.Init() only when // the configuration files have been modified. // == System configuration files. const fileLogin = "/etc/login.defs" type confLogin struct { PASS_MIN_DAYS int PASS_MAX_DAYS int PASS_MIN_LEN int PASS_WARN_AGE int SYS_UID_MIN int SYS_UID_MAX int SYS_GID_MIN int SYS_GID_MAX int UID_MIN int UID_MAX int GID_MIN int GID_MAX int ENCRYPT_METHOD string // upper SHA_CRYPT_MIN_ROUNDS int SHA_CRYPT_MAX_ROUNDS int // or CRYPT_PREFIX string // $2a$ CRYPT_ROUNDS int // 8 } const fileUseradd = "/etc/default/useradd" type confUseradd struct { HOME string // Default to '/home' SHELL string // Default to '/bin/sh' } // == Optional files. // Used in systems derivated from Debian: Ubuntu, Mint. const fileAdduser = "/etc/adduser.conf" type confAdduser struct { FIRST_SYSTEM_UID int LAST_SYSTEM_UID int FIRST_SYSTEM_GID int LAST_SYSTEM_GID int FIRST_UID int LAST_UID int FIRST_GID int LAST_GID int } // Used in Arch, Manjaro, OpenSUSE. // But it is only used by 'pam_unix2.so'. const filePasswd = "/etc/default/passwd" // TODO: to see the other options of that file. type confPasswd struct { CRYPT string // lower } // Used in systems derivated from Red Hat: CentOS, Fedora, Mageia, PCLinuxOS. const fileLibuser = "/etc/libuser.conf" type confLibuser struct { login_defs string crypt_style string // lower // For SHA2 hash_rounds_min int hash_rounds_max int } // * * * var debug bool // For testing // A configData represents the configuration used to add users and groups. type configData struct { login confLogin useradd confUseradd crypter crypt.Crypter sync.Once } var config configData // init sets the configuration data. func (c *configData) init() error { _confLogin := &confLogin{} cfg, err := shconf.ParseFile(fileLogin) if err != nil { return err } if err = cfg.Unmarshal(_confLogin); err != nil { return err } if debug { fmt.Printf("\n* %s\n", fileLogin) internal.PrintStruct(_confLogin) } if _confLogin.PASS_MAX_DAYS == 0 { _confLogin.PASS_MAX_DAYS = 99999 } if _confLogin.PASS_WARN_AGE == 0 { _confLogin.PASS_WARN_AGE = 7 } cfg, err = shconf.ParseFile(fileUseradd) if err != nil { return err } _confUseradd := &confUseradd{} if err = cfg.Unmarshal(_confUseradd); err != nil { return err } if debug { fmt.Printf("\n* %s\n", fileUseradd) internal.PrintStruct(_confUseradd) } if _confUseradd.HOME == "" { _confUseradd.HOME = "/home" } if _confUseradd.SHELL == "" { _confUseradd.SHELL = "/bin/sh" } config.useradd = *_confUseradd // Optional files found, err := exist(fileAdduser) // Based in Debian. if found { cfg, err := shconf.ParseFile(fileAdduser) if err != nil { return err } _confAdduser := &confAdduser{} if err = cfg.Unmarshal(_confAdduser); err != nil { return err } if debug { fmt.Printf("\n* %s\n", fileAdduser) internal.PrintStruct(_confAdduser) } if _confLogin.SYS_UID_MIN == 0 || _confLogin.SYS_UID_MAX == 0 || _confLogin.SYS_GID_MIN == 0 || _confLogin.SYS_GID_MAX == 0 || _confLogin.UID_MIN == 0 || _confLogin.UID_MAX == 0 || _confLogin.GID_MIN == 0 || _confLogin.GID_MAX == 0 { _confLogin.SYS_UID_MIN = _confAdduser.FIRST_SYSTEM_UID _confLogin.SYS_UID_MAX = _confAdduser.LAST_SYSTEM_UID _confLogin.SYS_GID_MIN = _confAdduser.FIRST_SYSTEM_GID _confLogin.SYS_GID_MAX = _confAdduser.LAST_SYSTEM_GID _confLogin.UID_MIN = _confAdduser.FIRST_UID _confLogin.UID_MAX = _confAdduser.LAST_UID _confLogin.GID_MIN = _confAdduser.FIRST_GID _confLogin.GID_MAX = _confAdduser.LAST_GID } } else if err != nil { return err } else if found, err = exist(fileLibuser); found { // Based in Red Hat. cfg, err := shconf.ParseFile(fileLibuser) if err != nil { return err } _confLibuser := &confLibuser{} if err = cfg.Unmarshal(_confLibuser); err != nil { return err } if debug { fmt.Printf("\n* %s\n", fileLibuser) internal.PrintStruct(_confLibuser) } if _confLibuser.login_defs != fileLogin { _confLogin.ENCRYPT_METHOD = _confLibuser.crypt_style _confLogin.SHA_CRYPT_MIN_ROUNDS = _confLibuser.hash_rounds_min _confLogin.SHA_CRYPT_MAX_ROUNDS = _confLibuser.hash_rounds_max } } else if err != nil { return err } /*else if found, err = exist(filePasswd); found { cfg, err := shconf.ParseFile(filePasswd) if err != nil { return err } _confPasswd := &confPasswd{} if err = cfg.Unmarshal(_confPasswd); err != nil { return err } if debug { fmt.Printf("\n* %s\n", filePasswd) internal.PrintStruct(_confPasswd) } if _confPasswd.CRYPT != "" { _confLogin.ENCRYPT_METHOD = _confPasswd.CRYPT } } else if err != nil { return err }*/ switch strings.ToUpper(_confLogin.ENCRYPT_METHOD) { case "MD5": c.crypter = crypt.New(crypt.MD5) case "SHA256": c.crypter = crypt.New(crypt.SHA256) case "SHA512": c.crypter = crypt.New(crypt.SHA512) case "": if c.crypter, err = lookupCrypter(); err != nil { return err } default: return fmt.Errorf("user: requested cryp function is unavailable: %s", c.login.ENCRYPT_METHOD) } if _confLogin.SYS_UID_MIN == 0 || _confLogin.SYS_UID_MAX == 0 || _confLogin.SYS_GID_MIN == 0 || _confLogin.SYS_GID_MAX == 0 || _confLogin.UID_MIN == 0 || _confLogin.UID_MAX == 0 || _confLogin.GID_MIN == 0 || _confLogin.GID_MAX == 0 { _confLogin.SYS_UID_MIN = 100 _confLogin.SYS_UID_MAX = 999 _confLogin.SYS_GID_MIN = 100 _confLogin.SYS_GID_MAX = 999 _confLogin.UID_MIN = 1000 _confLogin.UID_MAX = 29999 _confLogin.GID_MIN = 1000 _confLogin.GID_MAX = 29999 } config.login = *_confLogin return nil } // loadConfig loads user configuration. // It has to be loaded before of edit some file. func loadConfig() { config.Do(func() { //checkRoot() if err := config.init(); err != nil { panic(err) } }) } osutil-1.5.0/user/config_test.go000066400000000000000000000005471462760513500166640ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import "testing" func TestLoadConfig(t *testing.T) { if testing.Verbose() { debug = true } loadConfig() } osutil-1.5.0/user/crypt.go000066400000000000000000000066611462760513500155240ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Passwords // // If the passwd field contains some string that is not a valid result of // hashing, for instance "!" or "*", the user will not be able to use a unix // passwd to log in (but the user may log in the system by other means). // // A passwd field which starts with a exclamation mark means that the passwd is // locked. The remaining characters on the line represent the passwd field before // the passwd was locked. package user import ( "bufio" "errors" "io" "log" "os" "github.com/tredoe/osutil/user/crypt" _ "github.com/tredoe/osutil/user/crypt/md5_crypt" _ "github.com/tredoe/osutil/user/crypt/sha256_crypt" _ "github.com/tredoe/osutil/user/crypt/sha512_crypt" //_ "github.com/tredoe/osutil/user/crypt/bcrypt" ) const lockChar = '!' // Character added at the beginning of the passwd to lock it. var ErrShadowPasswd = errors.New("no found user with shadowed passwd") // lookupCrypter returns the first crypt function found in shadowed passwd file. func lookupCrypter() (crypt.Crypter, error) { f, err := os.Open(fileShadow) if err != nil { return nil, err } defer f.Close() buf := bufio.NewReader(f) for { line, _, err := buf.ReadLine() if err != nil { if err == io.EOF { return nil, ErrShadowPasswd } log.Print(err) continue } shadow, err := parseShadow(string(line)) if err != nil { log.Print(err) continue } if shadow.password[0] == '$' { return crypt.NewFromHash(shadow.password), nil } } //return nil, ErrShadowPasswd } // SetCrypter sets the crypt function to can hash the passwords. // The type "crypt.Crypt" comes from package "github.com/tredoe/osutil/user/crypt". func SetCrypter(c crypt.Crypt) { loadConfig() config.crypter = crypt.New(c) } // Passwd sets a hashed passwd for the actual user. // The passwd must be supplied in clear-text. func (s *Shadow) Passwd(key []byte) { loadConfig() s.password, _ = config.crypter.Generate(key, nil) s.setChange() } // Passwd sets a hashed passwd for the actual group. // The passwd must be supplied in clear-text. func (gs *GShadow) Passwd(key []byte) { loadConfig() gs.password, _ = config.crypter.Generate(key, nil) } // == Change passwd // ChPasswd updates passwd. // The passwd must be supplied in clear-text. func ChPasswd(user string, key []byte) error { shadow, err := LookupShadow(user) if err != nil { return err } shadow.Passwd(key) return edit(user, shadow) } // ChGPasswd updates group passwd. // The passwd must be supplied in clear-text. func ChGPasswd(group string, key []byte) error { gshadow, err := LookupGShadow(group) if err != nil { return err } gshadow.Passwd(key) return edit(group, gshadow) } // == Locking // LockUser locks the passwd of the given user. func LockUser(name string) error { shadow, err := LookupShadow(name) if err != nil { return err } if shadow.password[0] != lockChar { shadow.password = string(lockChar) + shadow.password return edit(name, shadow) } return nil } // UnlockUser unlocks the passwd of the given user. func UnlockUser(name string) error { shadow, err := LookupShadow(name) if err != nil { return err } if shadow.password[0] == lockChar { shadow.password = shadow.password[1:] return edit(name, shadow) } return nil } osutil-1.5.0/user/crypt/000077500000000000000000000000001462760513500151645ustar00rootroot00000000000000osutil-1.5.0/user/crypt/AUTHORS.md000066400000000000000000000001761462760513500166370ustar00rootroot00000000000000### Initial author [Jeramey Crawford](https://github.com/jeramey) ### Other authors [Jonas mg](https://github.com/tredoe) osutil-1.5.0/user/crypt/LICENSE000066400000000000000000000025221462760513500161720ustar00rootroot00000000000000Copyright (c) 2012, Jeramey Crawford Copyright (c) 2013, Jonas mg All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. osutil-1.5.0/user/crypt/README.md000066400000000000000000000014241462760513500164440ustar00rootroot00000000000000crypt ===== A password hashing library. The goal of crypt is to bring a library of many common and popular password hashing algorithms to Go and to provide a simple and consistent interface to each of them. As every hashing method is implemented in pure Go, this library should be as portable as Go itself. All hashing methods come with a test suite which verifies their operation against itself as well as the output of other password hashing implementations to ensure compatibility with them. I hope you find this library to be useful and easy to use! Note: forked from ## Installation go get github.com/tredoe/osutil/user/crypt ## License The source files are distributed under a BSD-style license that can be found in the LICENSE file. osutil-1.5.0/user/crypt/apr1_crypt/000077500000000000000000000000001462760513500172505ustar00rootroot00000000000000osutil-1.5.0/user/crypt/apr1_crypt/apr1_crypt.go000066400000000000000000000031251462760513500216640ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package apr1_crypt implements the standard Unix MD5-crypt algorithm created // by Poul-Henning Kamp for FreeBSD, and modified by the Apache project. // // The only change from MD5-crypt is the use of the magic constant "$apr1$" // instead of "$1$". The algorithms are otherwise identical. package apr1_crypt import ( "github.com/tredoe/osutil/user/crypt" "github.com/tredoe/osutil/user/crypt/common" "github.com/tredoe/osutil/user/crypt/md5_crypt" ) func init() { crypt.RegisterCrypt(crypt.APR1, New, MagicPrefix) } const ( MagicPrefix = "$apr1$" SaltLenMin = 1 SaltLenMax = 8 RoundsDefault = 1000 ) var md5Crypt = md5_crypt.New() func init() { md5Crypt.SetSalt(GetSalt()) } type crypter struct{ Salt common.Salt } // New returns a new crypt.Crypter computing the variant "apr1" of MD5-crypt func New() crypt.Crypter { return &crypter{common.Salt{}} } func (c *crypter) Generate(key, salt []byte) (string, error) { return md5Crypt.Generate(key, salt) } func (c *crypter) Verify(hashedKey string, key []byte) error { return md5Crypt.Verify(hashedKey, key) } func (c *crypter) Cost(hashedKey string) (int, error) { return RoundsDefault, nil } func (c *crypter) SetSalt(salt common.Salt) {} func GetSalt() common.Salt { return common.Salt{ MagicPrefix: []byte(MagicPrefix), SaltLenMin: SaltLenMin, SaltLenMax: SaltLenMax, RoundsDefault: RoundsDefault, } } osutil-1.5.0/user/crypt/apr1_crypt/apr1_crypt_test.go000066400000000000000000000037041462760513500227260ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package apr1_crypt import "testing" var apr1Crypt = New() func TestGenerate(t *testing.T) { data := []struct { salt []byte key []byte out string }{ { []byte("$apr1$$"), []byte("abcdefghijk"), "$apr1$$NTjzQjNZnhYRPxN6ryN191", }, { []byte("$apr1$an overlong salt$"), []byte("abcdefgh"), "$apr1$an overl$iroRZrWCEoQojCkf6p8LC0", }, { []byte("$apr1$12345678$"), []byte("Lorem ipsum dolor sit amet"), "$apr1$12345678$/DpfgRGBHG8N0cbkmw0Fk/", }, { []byte("$apr1$deadbeef$"), []byte("password"), "$apr1$deadbeef$NWLhx1Ai4ScyoaAboTFco.", }, { []byte("$apr1$$"), []byte("missing salt"), "$apr1$$EcorjwkoQz4mYcksVEk6j0", }, { []byte("$apr1$holy-moly-batman$"), []byte("1234567"), "$apr1$holy-mol$/WX0350ZUEkvQkrrVJsrU.", }, { []byte("$apr1$asdfjkl;$"), []byte("A really long password. " + "Longer than a password has any righ" + "t to be. Hey bub, don't mess with t" + "his password."), "$apr1$asdfjkl;$2MbDUb/Bj6qcIIf38PXzp0", }, } for i, d := range data { hash, err := apr1Crypt.Generate(d.key, d.salt) if err != nil { t.Fatal(err) } if hash != d.out { t.Errorf("Test %d failed\nExpected: %s, got: %s", i, d.out, hash) } } } func TestVerify(t *testing.T) { data := [][]byte{ []byte("password"), []byte("12345"), []byte("That's amazing! I've got the same combination on my luggage!"), []byte("And change the combination on my luggage!"), []byte(" random spa c ing."), []byte("94ajflkvjzpe8u3&*j1k513KLJ&*()"), } for i, d := range data { hash, err := apr1Crypt.Generate(d, nil) if err != nil { t.Fatal(err) } if err = apr1Crypt.Verify(hash, d); err != nil { t.Errorf("Test %d failed: %s", i, d) } } } osutil-1.5.0/user/crypt/common/000077500000000000000000000000001462760513500164545ustar00rootroot00000000000000osutil-1.5.0/user/crypt/common/base64.go000066400000000000000000000032421462760513500200700ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package common const alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" // Base64_24Bit is a variant of Base64 encoding, commonly used with password // hashing algorithms to encode the result of their checksum output. // // The algorithm operates on up to 3 bytes at a time, encoding the following // 6-bit sequences into up to 4 hash64 ASCII bytes. // // 1. Bottom 6 bits of the first byte // 2. Top 2 bits of the first byte, and bottom 4 bits of the second byte. // 3. Top 4 bits of the second byte, and bottom 2 bits of the third byte. // 4. Top 6 bits of the third byte. // // This encoding method does not emit padding bytes as Base64 does. func Base64_24Bit(src []byte) (hash []byte) { if len(src) == 0 { return []byte{} // TODO: return nil } hashSize := (len(src) * 8) / 6 if (len(src) % 6) != 0 { hashSize += 1 } hash = make([]byte, hashSize) dst := hash for len(src) > 0 { switch len(src) { default: dst[0] = alphabet[src[0]&0x3f] dst[1] = alphabet[((src[0]>>6)|(src[1]<<2))&0x3f] dst[2] = alphabet[((src[1]>>4)|(src[2]<<4))&0x3f] dst[3] = alphabet[(src[2]>>2)&0x3f] src = src[3:] dst = dst[4:] case 2: dst[0] = alphabet[src[0]&0x3f] dst[1] = alphabet[((src[0]>>6)|(src[1]<<2))&0x3f] dst[2] = alphabet[(src[1]>>4)&0x3f] src = src[2:] dst = dst[3:] case 1: dst[0] = alphabet[src[0]&0x3f] dst[1] = alphabet[(src[0]>>6)&0x3f] src = src[1:] dst = dst[2:] } } return } osutil-1.5.0/user/crypt/common/doc.go000066400000000000000000000007071462760513500175540ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package common contains routines used by multiple password hashing // algorithms. // // Generally, you will never import this package directly. Many of the // *_crypt packages will import this package if they require it. package common osutil-1.5.0/user/crypt/common/salt.go000066400000000000000000000046531462760513500177560ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package common import ( "crypto/rand" "errors" "strconv" ) var ( ErrSaltPrefix = errors.New("invalid magic prefix") ErrSaltFormat = errors.New("invalid salt format") ErrSaltRounds = errors.New("invalid rounds") ) // Salt represents a salt. type Salt struct { MagicPrefix []byte SaltLenMin int SaltLenMax int RoundsMin int RoundsMax int RoundsDefault int } // Generate generates a random salt of a given length. // // The length is set thus: // // length > SaltLenMax: length = SaltLenMax // length < SaltLenMin: length = SaltLenMin func (s *Salt) Generate(length int) []byte { if length > s.SaltLenMax { length = s.SaltLenMax } else if length < s.SaltLenMin { length = s.SaltLenMin } saltLen := (length * 6 / 8) if (length*6)%8 != 0 { saltLen++ } salt := make([]byte, saltLen) rand.Read(salt) out := make([]byte, len(s.MagicPrefix)+length) copy(out, s.MagicPrefix) copy(out[len(s.MagicPrefix):], Base64_24Bit(salt)) return out } // GenerateWRounds creates a random salt with the random bytes being of the // length provided, and the rounds parameter set as specified. // // The parameters are set thus: // // length > SaltLenMax: length = SaltLenMax // length < SaltLenMin: length = SaltLenMin // // rounds < 0: rounds = RoundsDefault // rounds < RoundsMin: rounds = RoundsMin // rounds > RoundsMax: rounds = RoundsMax // // If rounds is equal to RoundsDefault, then the "rounds=" part of the salt is // removed. func (s *Salt) GenerateWRounds(length, rounds int) []byte { if length > s.SaltLenMax { length = s.SaltLenMax } else if length < s.SaltLenMin { length = s.SaltLenMin } if rounds < 0 { rounds = s.RoundsDefault } else if rounds < s.RoundsMin { rounds = s.RoundsMin } else if rounds > s.RoundsMax { rounds = s.RoundsMax } saltLen := (length * 6 / 8) if (length*6)%8 != 0 { saltLen++ } salt := make([]byte, saltLen) rand.Read(salt) roundsText := "" if rounds != s.RoundsDefault { roundsText = "rounds=" + strconv.Itoa(rounds) + "$" } out := make([]byte, len(s.MagicPrefix)+len(roundsText)+length) copy(out, s.MagicPrefix) copy(out[len(s.MagicPrefix):], []byte(roundsText)) copy(out[len(s.MagicPrefix)+len(roundsText):], Base64_24Bit(salt)) return out } osutil-1.5.0/user/crypt/common/salt_test.go000066400000000000000000000025061462760513500210100ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package common import ( "testing" "strings" "strconv" ) var _Salt = &Salt{ MagicPrefix: []byte("$foo$"), SaltLenMin: 1, SaltLenMax: 8, RoundsMin: 1000, RoundsMax: 999999999, RoundsDefault: 5000, } func TestGenerateSalt(t *testing.T) { magicPrefixLen := len(_Salt.MagicPrefix) salt := _Salt.Generate(0) if len(salt) != magicPrefixLen+1 { t.Errorf("Expected len 1, got len %d", len(salt)) } for i := _Salt.SaltLenMin; i <= _Salt.SaltLenMax; i++ { salt = _Salt.Generate(i) if len(salt) != magicPrefixLen+i { t.Errorf("Expected len %d, got len %d", i, len(salt)) } } salt = _Salt.Generate(9) if len(salt) != magicPrefixLen+8 { t.Errorf("Expected len 8, got len %d", len(salt)) } } func TestGenerateSaltWRounds(t *testing.T) { const rounds = 5001 salt := _Salt.GenerateWRounds(_Salt.SaltLenMax, rounds) if salt == nil { t.Errorf("salt should not be nil") } expectedPrefix := string(_Salt.MagicPrefix) + "rounds=" + strconv.Itoa(rounds) + "$" if !strings.HasPrefix(string(salt), expectedPrefix) { t.Errorf("salt '%s' should start with prefix '%s' but didn't", salt, expectedPrefix) } } osutil-1.5.0/user/crypt/crypt.go000066400000000000000000000067011462760513500166600ustar00rootroot00000000000000// Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package crypt provides interface for password crypt functions and collects // common constants. package crypt import ( "errors" "strings" "github.com/tredoe/osutil/user/crypt/common" ) var ErrKeyMismatch = errors.New("hashed value is not the hash of the given password") // Crypter is the common interface implemented by all crypt functions. type Crypter interface { // Generate performs the hashing algorithm, returning a full hash suitable // for storage and later password verification. // // If the salt is empty, a randomly-generated salt will be generated with a // length of SaltLenMax and number RoundsDefault of rounds. // // Any error only can be got when the salt argument is not empty. Generate(key, salt []byte) (string, error) // Verify compares a hashed key with its possible key equivalent. // Returns nil on success, or an error on failure; if the hashed key is // diffrent, the error is "ErrKeyMismatch". Verify(hashedKey string, key []byte) error // Cost returns the hashing cost (in rounds) used to create the given hashed // key. // // When, in the future, the hashing cost of a key needs to be increased in // order to adjust for greater computational power, this function allows one // to establish which keys need to be updated. // // The algorithms based in MD5-crypt use a fixed value of rounds. Cost(hashedKey string) (int, error) // SetSalt sets a different salt. It is used to easily create derivated // algorithms, i.e. "apr1_crypt" from "md5_crypt". SetSalt(salt common.Salt) } // Crypt identifies a crypt function that is implemented in another package. type Crypt uint const ( APR1 Crypt = iota + 1 // import "github.com/tredoe/osutil/user/crypt/apr1_crypt" MD5 // import "github.com/tredoe/osutil/user/crypt/md5_crypt" SHA256 // import "github.com/tredoe/osutil/user/crypt/sha256_crypt" SHA512 // import "github.com/tredoe/osutil/user/crypt/sha512_crypt" maxCrypt ) var cryptPrefixes = make([]string, maxCrypt) var crypts = make([]func() Crypter, maxCrypt) // RegisterCrypt registers a function that returns a new instance of the given // crypt function. This is intended to be called from the init function in // packages that implement crypt functions. func RegisterCrypt(c Crypt, f func() Crypter, prefix string) { if c >= maxCrypt { panic("crypt: RegisterHash of unknown crypt function") } crypts[c] = f cryptPrefixes[c] = prefix } // New returns a new crypter. func New(c Crypt) Crypter { f := crypts[c] if f != nil { return f() } panic("crypt: requested crypt function is unavailable") } // NewFromHash returns a new Crypter using the prefix in the given hashed key. func NewFromHash(hashedKey string) Crypter { var f func() Crypter if strings.HasPrefix(hashedKey, cryptPrefixes[SHA512]) { f = crypts[SHA512] } else if strings.HasPrefix(hashedKey, cryptPrefixes[SHA256]) { f = crypts[SHA256] } else if strings.HasPrefix(hashedKey, cryptPrefixes[MD5]) { f = crypts[MD5] } else if strings.HasPrefix(hashedKey, cryptPrefixes[APR1]) { f = crypts[APR1] } else { toks := strings.SplitN(hashedKey, "$", 3) prefix := "$" + toks[1] + "$" panic("crypt: unknown cryp function from prefix: " + prefix) } if f != nil { return f() } panic("crypt: requested cryp function is unavailable") } osutil-1.5.0/user/crypt/md5_crypt/000077500000000000000000000000001462760513500170725ustar00rootroot00000000000000osutil-1.5.0/user/crypt/md5_crypt/md5_crypt.go000066400000000000000000000073711462760513500213370ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package md5_crypt implements the standard Unix MD5-crypt algorithm created by // Poul-Henning Kamp for FreeBSD. package md5_crypt import ( "bytes" "crypto/md5" "github.com/tredoe/osutil/user/crypt" "github.com/tredoe/osutil/user/crypt/common" ) func init() { crypt.RegisterCrypt(crypt.MD5, New, MagicPrefix) } // NOTE: Cisco IOS only allows salts of length 4. const ( MagicPrefix = "$1$" SaltLenMin = 1 // Real minimum is 0, but that isn't useful. SaltLenMax = 8 RoundsDefault = 1000 ) type crypter struct{ Salt common.Salt } // New returns a new crypt.Crypter computing the MD5-crypt password hashing. func New() crypt.Crypter { return &crypter{GetSalt()} } func (c *crypter) Generate(key, salt []byte) (string, error) { if len(salt) == 0 { salt = c.Salt.Generate(SaltLenMax) } if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) { return "", common.ErrSaltPrefix } saltToks := bytes.Split(salt, []byte{'$'}) if len(saltToks) < 3 { return "", common.ErrSaltFormat } else { salt = saltToks[2] } if len(salt) > 8 { salt = salt[0:8] } // Compute alternate MD5 sum with input KEY, SALT, and KEY. Alternate := md5.New() Alternate.Write(key) Alternate.Write(salt) Alternate.Write(key) AlternateSum := Alternate.Sum(nil) // 16 bytes A := md5.New() A.Write(key) A.Write(c.Salt.MagicPrefix) A.Write(salt) // Add for any character in the key one byte of the alternate sum. i := len(key) for ; i > 16; i -= 16 { A.Write(AlternateSum) } A.Write(AlternateSum[0:i]) // The original implementation now does something weird: // For every 1 bit in the key, the first 0 is added to the buffer // For every 0 bit, the first character of the key // This does not seem to be what was intended but we have to follow this to // be compatible. for i = len(key); i > 0; i >>= 1 { if (i & 1) == 0 { A.Write(key[0:1]) } else { A.Write([]byte{0}) } } Csum := A.Sum(nil) // In fear of password crackers here comes a quite long loop which just // processes the output of the previous round again. // We cannot ignore this here. for i = 0; i < RoundsDefault; i++ { C := md5.New() // Add key or last result. if (i & 1) != 0 { C.Write(key) } else { C.Write(Csum) } // Add salt for numbers not divisible by 3. if (i % 3) != 0 { C.Write(salt) } // Add key for numbers not divisible by 7. if (i % 7) != 0 { C.Write(key) } // Add key or last result. if (i & 1) == 0 { C.Write(key) } else { C.Write(Csum) } Csum = C.Sum(nil) } out := make([]byte, 0, 23+len(c.Salt.MagicPrefix)+len(salt)) out = append(out, c.Salt.MagicPrefix...) out = append(out, salt...) out = append(out, '$') out = append(out, common.Base64_24Bit([]byte{ Csum[12], Csum[6], Csum[0], Csum[13], Csum[7], Csum[1], Csum[14], Csum[8], Csum[2], Csum[15], Csum[9], Csum[3], Csum[5], Csum[10], Csum[4], Csum[11], })...) // Clean sensitive data. A.Reset() Alternate.Reset() for i = 0; i < len(AlternateSum); i++ { AlternateSum[i] = 0 } return string(out), nil } func (c *crypter) Verify(hashedKey string, key []byte) error { newHash, err := c.Generate(key, []byte(hashedKey)) if err != nil { return err } if newHash != hashedKey { return crypt.ErrKeyMismatch } return nil } func (c *crypter) Cost(hashedKey string) (int, error) { return RoundsDefault, nil } func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt } func GetSalt() common.Salt { return common.Salt{ MagicPrefix: []byte(MagicPrefix), SaltLenMin: SaltLenMin, SaltLenMax: SaltLenMax, RoundsDefault: RoundsDefault, } } osutil-1.5.0/user/crypt/md5_crypt/md5_crypt_test.go000066400000000000000000000036141462760513500223720ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package md5_crypt import "testing" var md5Crypt = New() func TestGenerate(t *testing.T) { data := []struct { salt []byte key []byte out string }{ { []byte("$1$$"), []byte("abcdefghijk"), "$1$$pL/BYSxMXs.jVuSV1lynn1", }, { []byte("$1$an overlong salt$"), []byte("abcdfgh"), "$1$an overl$ZYftmJDIw8sG5s4gG6r.70", }, { []byte("$1$12345678$"), []byte("Lorem ipsum dolor sit amet"), "$1$12345678$Suzx8CrBlkNJwVHHHv5tZ.", }, { []byte("$1$deadbeef$"), []byte("password"), "$1$deadbeef$Q7g0UO4hRC0mgQUQ/qkjZ0", }, { []byte("$1$$"), []byte("missing salt"), "$1$$Lv61fbMiEGprscPkdE9Iw/", }, { []byte("$1$holy-moly-batman$"), []byte("1234567"), "$1$holy-mol$WKomB0dWknSxdW/e8WYHG0", }, { []byte("$1$asdfjkl;$"), []byte("A really long password. Longer " + "than a password has any right to be" + ". Hey bub, don't mess with this password."), "$1$asdfjkl;$DUqPhKwbK4smV0aEMyDdx/", }, } for i, d := range data { hash, err := md5Crypt.Generate(d.key, d.salt) if err != nil { t.Fatal(err) } if hash != d.out { t.Errorf("Test %d failed\nExpected: %s, got: %s", i, d.out, hash) } } } func TestVerify(t *testing.T) { data := [][]byte{ []byte("password"), []byte("12345"), []byte("That's amazing! I've got the same combination on my luggage!"), []byte("And change the combination on my luggage!"), []byte(" random spa c ing."), []byte("94ajflkvjzpe8u3&*j1k513KLJ&*()"), } for i, d := range data { hash, err := md5Crypt.Generate(d, nil) if err != nil { t.Fatal(err) } if err = md5Crypt.Verify(hash, d); err != nil { t.Errorf("Test %d failed: %s", i, d) } } } osutil-1.5.0/user/crypt/sha256_crypt/000077500000000000000000000000001462760513500174155ustar00rootroot00000000000000osutil-1.5.0/user/crypt/sha256_crypt/sha256_crypt.go000066400000000000000000000125621462760513500222030ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package sha256_crypt implements Ulrich Drepper's SHA256-crypt password // hashing algorithm. // // The specification for this algorithm can be found here: // http://www.akkadia.org/drepper/SHA-crypt.txt package sha256_crypt import ( "bytes" "crypto/sha256" "strconv" "github.com/tredoe/osutil/user/crypt" "github.com/tredoe/osutil/user/crypt/common" ) func init() { crypt.RegisterCrypt(crypt.SHA256, New, MagicPrefix) } const ( MagicPrefix = "$5$" SaltLenMin = 1 SaltLenMax = 16 RoundsMin = 1000 RoundsMax = 999999999 RoundsDefault = 5000 ) var _rounds = []byte("rounds=") type crypter struct{ Salt common.Salt } // New returns a new crypt.Crypter computing the SHA256-crypt password hashing. func New() crypt.Crypter { return &crypter{GetSalt()} } func (c *crypter) Generate(key, salt []byte) (string, error) { var rounds int var isRoundsDef bool if len(salt) == 0 { salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault) } if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) { return "", common.ErrSaltPrefix } saltToks := bytes.Split(salt, []byte{'$'}) if len(saltToks) < 3 { return "", common.ErrSaltFormat } if bytes.HasPrefix(saltToks[2], _rounds) { isRoundsDef = true pr, err := strconv.ParseInt(string(saltToks[2][7:]), 10, 32) if err != nil { return "", common.ErrSaltRounds } rounds = int(pr) if rounds < RoundsMin { rounds = RoundsMin } else if rounds > RoundsMax { rounds = RoundsMax } salt = saltToks[3] } else { rounds = RoundsDefault salt = saltToks[2] } if len(salt) > 16 { salt = salt[0:16] } // Compute alternate SHA256 sum with input KEY, SALT, and KEY. Alternate := sha256.New() Alternate.Write(key) Alternate.Write(salt) Alternate.Write(key) AlternateSum := Alternate.Sum(nil) // 32 bytes A := sha256.New() A.Write(key) A.Write(salt) // Add for any character in the key one byte of the alternate sum. i := len(key) for ; i > 32; i -= 32 { A.Write(AlternateSum) } A.Write(AlternateSum[0:i]) // Take the binary representation of the length of the key and for every add // the alternate sum, for every 0 the key. for i = len(key); i > 0; i >>= 1 { if (i & 1) != 0 { A.Write(AlternateSum) } else { A.Write(key) } } Asum := A.Sum(nil) // Start computation of P byte sequence. P := sha256.New() // For every character in the password add the entire password. for i = 0; i < len(key); i++ { P.Write(key) } Psum := P.Sum(nil) // Create byte sequence P. Pseq := make([]byte, 0, len(key)) for i = len(key); i > 32; i -= 32 { Pseq = append(Pseq, Psum...) } Pseq = append(Pseq, Psum[0:i]...) // Start computation of S byte sequence. S := sha256.New() for i = 0; i < (16 + int(Asum[0])); i++ { S.Write(salt) } Ssum := S.Sum(nil) // Create byte sequence S. Sseq := make([]byte, 0, len(salt)) for i = len(salt); i > 32; i -= 32 { Sseq = append(Sseq, Ssum...) } Sseq = append(Sseq, Ssum[0:i]...) Csum := Asum // Repeatedly run the collected hash value through SHA256 to burn CPU cycles. for i = 0; i < rounds; i++ { C := sha256.New() // Add key or last result. if (i & 1) != 0 { C.Write(Pseq) } else { C.Write(Csum) } // Add salt for numbers not divisible by 3. if (i % 3) != 0 { C.Write(Sseq) } // Add key for numbers not divisible by 7. if (i % 7) != 0 { C.Write(Pseq) } // Add key or last result. if (i & 1) != 0 { C.Write(Csum) } else { C.Write(Pseq) } Csum = C.Sum(nil) } out := make([]byte, 0, 80) out = append(out, c.Salt.MagicPrefix...) if isRoundsDef { out = append(out, []byte("rounds="+strconv.Itoa(rounds)+"$")...) } out = append(out, salt...) out = append(out, '$') out = append(out, common.Base64_24Bit([]byte{ Csum[20], Csum[10], Csum[0], Csum[11], Csum[1], Csum[21], Csum[2], Csum[22], Csum[12], Csum[23], Csum[13], Csum[3], Csum[14], Csum[4], Csum[24], Csum[5], Csum[25], Csum[15], Csum[26], Csum[16], Csum[6], Csum[17], Csum[7], Csum[27], Csum[8], Csum[28], Csum[18], Csum[29], Csum[19], Csum[9], Csum[30], Csum[31], })...) // Clean sensitive data. A.Reset() Alternate.Reset() P.Reset() for i = 0; i < len(Asum); i++ { Asum[i] = 0 } for i = 0; i < len(AlternateSum); i++ { AlternateSum[i] = 0 } for i = 0; i < len(Pseq); i++ { Pseq[i] = 0 } return string(out), nil } func (c *crypter) Verify(hashedKey string, key []byte) error { newHash, err := c.Generate(key, []byte(hashedKey)) if err != nil { return err } if newHash != hashedKey { return crypt.ErrKeyMismatch } return nil } func (c *crypter) Cost(hashedKey string) (int, error) { saltToks := bytes.Split([]byte(hashedKey), []byte{'$'}) if len(saltToks) < 3 { return 0, common.ErrSaltFormat } if !bytes.HasPrefix(saltToks[2], _rounds) { return RoundsDefault, nil } roundToks := bytes.Split(saltToks[2], []byte{'='}) cost, err := strconv.ParseInt(string(roundToks[1]), 10, 0) return int(cost), err } func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt } func GetSalt() common.Salt { return common.Salt{ MagicPrefix: []byte(MagicPrefix), SaltLenMin: SaltLenMin, SaltLenMax: SaltLenMax, RoundsDefault: RoundsDefault, RoundsMin: RoundsMin, RoundsMax: RoundsMax, } } osutil-1.5.0/user/crypt/sha256_crypt/sha256_crypt_test.go000066400000000000000000000052011462760513500232320ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package sha256_crypt import "testing" var sha256Crypt = New() func TestGenerate(t *testing.T) { data := []struct { salt []byte key []byte out string cost int }{ { []byte("$5$saltstring"), []byte("Hello world!"), "$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5", RoundsDefault, }, { []byte("$5$rounds=10000$saltstringsaltstring"), []byte("Hello world!"), "$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFM" + "z2.opqey6IcA", 10000, }, { []byte("$5$rounds=5000$toolongsaltstring"), []byte("This is just a test"), "$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPv" + "OW8mGRcvxa5", 5000, }, { []byte("$5$rounds=1400$anotherlongsaltstring"), []byte("a very much longer text to encrypt. " + "This one even stretches over more" + "than one line."), "$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyx" + "f12oP84Bnq1", 1400, }, { []byte("$5$rounds=77777$short"), []byte("we have a short salt string but not a short password"), "$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/", 77777, }, { []byte("$5$rounds=123456$asaltof16chars.."), []byte("a short string"), "$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPy" + "zV/cZKmF/wJvD", 123456, }, { []byte("$5$rounds=10$roundstoolow"), []byte("the minimum number is still observed"), "$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/g" + "L972bIC", 1000, }, } for i, d := range data { hash, err := sha256Crypt.Generate(d.key, d.salt) if err != nil { t.Fatal(err) } if hash != d.out { t.Errorf("Test %d failed\nExpected: %s, got: %s", i, d.out, hash) } cost, err := sha256Crypt.Cost(hash) if err != nil { t.Fatal(err) } if cost != d.cost { t.Errorf("Test %d failed\nExpected: %d, got: %d", i, d.cost, cost) } } } func TestVerify(t *testing.T) { data := [][]byte{ []byte("password"), []byte("12345"), []byte("That's amazing! I've got the same combination on my luggage!"), []byte("And change the combination on my luggage!"), []byte(" random spa c ing."), []byte("94ajflkvjzpe8u3&*j1k513KLJ&*()"), } for i, d := range data { hash, err := sha256Crypt.Generate(d, nil) if err != nil { t.Fatal(err) } if err = sha256Crypt.Verify(hash, d); err != nil { t.Errorf("Test %d failed: %s", i, d) } } } osutil-1.5.0/user/crypt/sha512_crypt/000077500000000000000000000000001462760513500174105ustar00rootroot00000000000000osutil-1.5.0/user/crypt/sha512_crypt/sha512_crypt.go000066400000000000000000000133311462760513500221640ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. // Package sha512_crypt implements Ulrich Drepper's SHA512-crypt password // hashing algorithm. // // The specification for this algorithm can be found here: // http://www.akkadia.org/drepper/SHA-crypt.txt package sha512_crypt import ( "bytes" "crypto/sha512" "strconv" "github.com/tredoe/osutil/user/crypt" "github.com/tredoe/osutil/user/crypt/common" ) func init() { crypt.RegisterCrypt(crypt.SHA512, New, MagicPrefix) } const ( MagicPrefix = "$6$" SaltLenMin = 1 SaltLenMax = 16 RoundsMin = 1000 RoundsMax = 999999999 RoundsDefault = 5000 ) var _rounds = []byte("rounds=") type crypter struct{ Salt common.Salt } // New returns a new crypt.Crypter computing the SHA512-crypt password hashing. func New() crypt.Crypter { return &crypter{GetSalt()} } func (c *crypter) Generate(key, salt []byte) (string, error) { var rounds int var isRoundsDef bool if len(salt) == 0 { salt = c.Salt.GenerateWRounds(SaltLenMax, RoundsDefault) } if !bytes.HasPrefix(salt, c.Salt.MagicPrefix) { return "", common.ErrSaltPrefix } saltToks := bytes.Split(salt, []byte{'$'}) if len(saltToks) < 3 { return "", common.ErrSaltFormat } if bytes.HasPrefix(saltToks[2], _rounds) { isRoundsDef = true pr, err := strconv.ParseInt(string(saltToks[2][7:]), 10, 32) if err != nil { return "", common.ErrSaltRounds } rounds = int(pr) if rounds < RoundsMin { rounds = RoundsMin } else if rounds > RoundsMax { rounds = RoundsMax } salt = saltToks[3] } else { rounds = RoundsDefault salt = saltToks[2] } if len(salt) > SaltLenMax { salt = salt[0:SaltLenMax] } // Compute alternate SHA512 sum with input KEY, SALT, and KEY. Alternate := sha512.New() Alternate.Write(key) Alternate.Write(salt) Alternate.Write(key) AlternateSum := Alternate.Sum(nil) // 64 bytes A := sha512.New() A.Write(key) A.Write(salt) // Add for any character in the key one byte of the alternate sum. i := len(key) for ; i > 64; i -= 64 { A.Write(AlternateSum) } A.Write(AlternateSum[0:i]) // Take the binary representation of the length of the key and for every add // the alternate sum, for every 0 the key. for i = len(key); i > 0; i >>= 1 { if (i & 1) != 0 { A.Write(AlternateSum) } else { A.Write(key) } } Asum := A.Sum(nil) // Start computation of P byte sequence. P := sha512.New() // For every character in the password add the entire password. for i = 0; i < len(key); i++ { P.Write(key) } Psum := P.Sum(nil) // Create byte sequence P. Pseq := make([]byte, 0, len(key)) for i = len(key); i > 64; i -= 64 { Pseq = append(Pseq, Psum...) } Pseq = append(Pseq, Psum[0:i]...) // Start computation of S byte sequence. S := sha512.New() for i = 0; i < (16 + int(Asum[0])); i++ { S.Write(salt) } Ssum := S.Sum(nil) // Create byte sequence S. Sseq := make([]byte, 0, len(salt)) for i = len(salt); i > 64; i -= 64 { Sseq = append(Sseq, Ssum...) } Sseq = append(Sseq, Ssum[0:i]...) Csum := Asum // Repeatedly run the collected hash value through SHA512 to burn CPU cycles. for i = 0; i < rounds; i++ { C := sha512.New() // Add key or last result. if (i & 1) != 0 { C.Write(Pseq) } else { C.Write(Csum) } // Add salt for numbers not divisible by 3. if (i % 3) != 0 { C.Write(Sseq) } // Add key for numbers not divisible by 7. if (i % 7) != 0 { C.Write(Pseq) } // Add key or last result. if (i & 1) != 0 { C.Write(Csum) } else { C.Write(Pseq) } Csum = C.Sum(nil) } out := make([]byte, 0, 123) out = append(out, c.Salt.MagicPrefix...) if isRoundsDef { out = append(out, []byte("rounds="+strconv.Itoa(rounds)+"$")...) } out = append(out, salt...) out = append(out, '$') out = append(out, common.Base64_24Bit([]byte{ Csum[42], Csum[21], Csum[0], Csum[1], Csum[43], Csum[22], Csum[23], Csum[2], Csum[44], Csum[45], Csum[24], Csum[3], Csum[4], Csum[46], Csum[25], Csum[26], Csum[5], Csum[47], Csum[48], Csum[27], Csum[6], Csum[7], Csum[49], Csum[28], Csum[29], Csum[8], Csum[50], Csum[51], Csum[30], Csum[9], Csum[10], Csum[52], Csum[31], Csum[32], Csum[11], Csum[53], Csum[54], Csum[33], Csum[12], Csum[13], Csum[55], Csum[34], Csum[35], Csum[14], Csum[56], Csum[57], Csum[36], Csum[15], Csum[16], Csum[58], Csum[37], Csum[38], Csum[17], Csum[59], Csum[60], Csum[39], Csum[18], Csum[19], Csum[61], Csum[40], Csum[41], Csum[20], Csum[62], Csum[63], })...) // Clean sensitive data. A.Reset() Alternate.Reset() P.Reset() for i = 0; i < len(Asum); i++ { Asum[i] = 0 } for i = 0; i < len(AlternateSum); i++ { AlternateSum[i] = 0 } for i = 0; i < len(Pseq); i++ { Pseq[i] = 0 } return string(out), nil } func (c *crypter) Verify(hashedKey string, key []byte) error { newHash, err := c.Generate(key, []byte(hashedKey)) if err != nil { return err } if newHash != hashedKey { return crypt.ErrKeyMismatch } return nil } func (c *crypter) Cost(hashedKey string) (int, error) { saltToks := bytes.Split([]byte(hashedKey), []byte{'$'}) if len(saltToks) < 3 { return 0, common.ErrSaltFormat } if !bytes.HasPrefix(saltToks[2], _rounds) { return RoundsDefault, nil } roundToks := bytes.Split(saltToks[2], []byte{'='}) cost, err := strconv.ParseInt(string(roundToks[1]), 10, 0) return int(cost), err } func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt } func GetSalt() common.Salt { return common.Salt{ MagicPrefix: []byte(MagicPrefix), SaltLenMin: SaltLenMin, SaltLenMax: SaltLenMax, RoundsDefault: RoundsDefault, RoundsMin: RoundsMin, RoundsMax: RoundsMax, } } osutil-1.5.0/user/crypt/sha512_crypt/sha512_crypt_test.go000066400000000000000000000057001462760513500232240ustar00rootroot00000000000000// Copyright 2012, Jeramey Crawford // Copyright 2013, Jonas mg // All rights reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file. package sha512_crypt import "testing" var sha512Crypt = New() func TestGenerate(t *testing.T) { data := []struct { salt []byte key []byte out string cost int }{ { []byte("$6$saltstring"), []byte("Hello world!"), "$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjn" + "QJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1", RoundsDefault, }, { []byte("$6$rounds=10000$saltstringsaltstring"), []byte("Hello world!"), "$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh" + "0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.", 10000, }, { []byte("$6$rounds=5000$toolongsaltstring"), []byte("This is just a test"), "$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoN" + "eKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0", 5000, }, { []byte("$6$rounds=1400$anotherlongsaltstring"), []byte("a very much longer text to encrypt. " + "This one even stretches over more" + "than one line."), "$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs" + ".wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1", 1400, }, { []byte("$6$rounds=77777$short"), []byte("we have a short salt string but not a short password"), "$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkv" + "r0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0", 77777, }, { []byte("$6$rounds=123456$asaltof16chars.."), []byte("a short string"), "$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4o" + "PwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1", 123456, }, { []byte("$6$rounds=10$roundstoolow"), []byte("the minimum number is still observed"), "$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50Yh" + "H1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.", 1000, }, } for i, d := range data { hash, err := sha512Crypt.Generate(d.key, d.salt) if err != nil { t.Fatal(err) } if hash != d.out { t.Errorf("Test %d failed\nExpected: %s, got: %s", i, d.out, hash) } cost, err := sha512Crypt.Cost(hash) if err != nil { t.Fatal(err) } if cost != d.cost { t.Errorf("Test %d failed\nExpected: %d, got: %d", i, d.cost, cost) } } } func TestVerify(t *testing.T) { data := [][]byte{ []byte("password"), []byte("12345"), []byte("That's amazing! I've got the same combination on my luggage!"), []byte("And change the combination on my luggage!"), []byte(" random spa c ing."), []byte("94ajflkvjzpe8u3&*j1k513KLJ&*()"), } for i, d := range data { hash, err := sha512Crypt.Generate(d, nil) if err != nil { t.Fatal(err) } if err = sha512Crypt.Verify(hash, d); err != nil { t.Errorf("Test %d failed: %s", i, d) } } } osutil-1.5.0/user/crypt_test.go000066400000000000000000000005601462760513500165530ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import "testing" func TestLookupCrypter(t *testing.T) { _, err := lookupCrypter() if err != nil { t.Fatal(err) } } osutil-1.5.0/user/dbfile.go000066400000000000000000000007411462760513500156010ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user // Since tests will be done in temporary files, there is to use variables to // change the values at testing. var ( fileUser = "/etc/passwd" fileGroup = "/etc/group" fileShadow = "/etc/shadow" fileGShadow = "/etc/gshadow" ) osutil-1.5.0/user/doc.go000066400000000000000000000011401462760513500151130ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. /* Package user provides access to UNIX users database in local files. You must have enough privileges to access to databases in shadowed files '/etc/shadow' and '/etc/gshadow'. This usually means have to be root. Note: those files are backed-up before of be modified. In testing, to print the configuration read from the system, there is to use "-v" flag. */ package user osutil-1.5.0/user/error.go000066400000000000000000000034161462760513500155070ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "errors" "fmt" "strconv" ) var ( ErrUserExist = errors.New("user already exists") ErrGroupExist = errors.New("group already exists") ) // IsExist returns whether the error is known to report that an user or group // already exists. It is satisfied by ErrUserExist and ErrGroupExist. func IsExist(err error) bool { if err == ErrUserExist || err == ErrGroupExist { return true } return false } // IdUsedError reports the presence of an identifier already used. type IdUsedError int func (e IdUsedError) Error() string { return "id used: " + strconv.Itoa(int(e)) } // A NoFoundError reports the absence of a value. type NoFoundError struct { file string field string value interface{} } func (e NoFoundError) Error() string { return fmt.Sprintf("entry \"%v\" not found: file '%s', field %q", e.value, e.file, e.field) } // A RequiredError reports the name of a required field. type RequiredError string func (e RequiredError) Error() string { return "required field: " + string(e) } // An atoiError records the file, row and field that caused the error at turning // a field from string to int. type atoiError struct { file string row string field string } func (e atoiError) Error() string { return fmt.Sprintf("field %q on '%s' could not be turned to int\n%s", e.field, e.file, e.row) } // A rowError records the file and row for a format not valid. type rowError struct { file string row string } func (e rowError) Error() string { return fmt.Sprintf("format of row not valid on '%s'\n%s", e.file, e.row) } osutil-1.5.0/user/file.go000066400000000000000000000076201462760513500152760ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "bufio" "bytes" "errors" "io" "os" "sync" "github.com/tredoe/osutil/file" ) // A row represents the structure of a row into a file. type row interface { // lookUp is the parser to looking for a value in the field of given line. lookUp(line string, _field field, value interface{}) interface{} // filename returns the file name belongs to the file structure. filename() string String() string } // A field represents a field into a row. type field interface { String() string } var errSearch = errors.New("no search") // lookUp is a generic parser to looking for a value. // // The count determines the number of fields to return: // n > 0: at most n fields // n == 0: the result is nil (zero fields) // n < 0: all fields func lookUp(_row row, _field field, value interface{}, n int) (interface{}, error) { if n == 0 { return nil, errSearch } dbf, err := openDBFile(_row.filename(), os.O_RDONLY) if err != nil { return nil, err } defer dbf.close() // Lines where a field is matched. entries := make([]interface{}, 0, 0) for { line, _, err := dbf.rd.ReadLine() if err == io.EOF { break } entry := _row.lookUp(string(line), _field, value) if entry != nil { entries = append(entries, entry) } if n < 0 { continue } else if n == len(entries) { break } } if len(entries) != 0 { return entries, nil } return nil, NoFoundError{_row.filename(), _field.String(), value} } // == Editing // // DO_BACKUP does a backup before of modify the original files. var DO_BACKUP = true // A dbfile represents the database file. type dbfile struct { sync.Mutex file *os.File rd *bufio.Reader } // openDBFile opens a file. func openDBFile(filename string, flag int) (*dbfile, error) { f, err := os.OpenFile(filename, flag, 0) if err != nil { return nil, err } db := &dbfile{file: f, rd: bufio.NewReader(f)} db.Lock() return db, nil } // close closes the file. func (db *dbfile) close() error { db.Unlock() return db.file.Close() } // filesBackuped are the files that already have been backuped. var filesBackuped = make(map[string]struct{}, 4) // backup does a backup of a file. func backup(filename string) error { if DO_BACKUP { if _, ok := filesBackuped[filename]; !ok { if err := file.Backup(filename); err != nil { return err } filesBackuped[filename] = struct{}{} } } return nil } func edit(name string, r row) error { return _edit(name, r, false) } func del(name string, r row) error { return _edit(name, r, true) } // _edit is a generic editor for the given user/group name. // If remove is true, it removes the structure of the user/group name. // // TODO: get better performance if start to store since when the file is edited. // So there is to store the size of all lines read until that point to seek from // there. func _edit(name string, _row row, remove bool) (err error) { filename := _row.filename() dbf, err := openDBFile(filename, os.O_RDWR) if err != nil { return err } defer func() { e := dbf.close() if e != nil && err == nil { err = e } }() var buf bytes.Buffer name_b := []byte(name) isFound := false for { line, err2 := dbf.rd.ReadBytes('\n') if err2 == io.EOF { break } if !isFound && bytes.HasPrefix(line, name_b) { isFound = true if remove { // skip user continue } line = []byte(_row.String()) } if _, err = buf.Write(line); err != nil { return err } } if isFound { if err = backup(filename); err != nil { return } if _, err = dbf.file.Seek(0, io.SeekStart); err != nil { return } var n int n, err = dbf.file.Write(buf.Bytes()) if err != nil { return } err = dbf.file.Truncate(int64(n)) } return } osutil-1.5.0/user/group.go000066400000000000000000000232751462760513500155170ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "errors" "fmt" "os" "reflect" "strconv" "strings" ) type groupField int // Field names for group database. const ( G_NAME groupField = 1 << iota G_PASSWD G_GID G_MEMBER G_ALL ) func (f groupField) String() string { switch f { case G_NAME: return "Name" case G_PASSWD: return "Passwd" case G_GID: return "GID" case G_MEMBER: return "Member" } return "ALL" } // A Group represents the format of a group on the system. type Group struct { // Group name. (Unique) Name string // Hashed password // // The (hashed) group password. If this field is empty, no password is needed. password string // The numeric group ID. (Unique) GID int // User list // // A list of the usernames that are members of this group, separated by commas. UserList []string addSystemGroup bool } // NewGroup returns a new Group. func NewGroup(name string, members ...string) *Group { return &Group{ Name: name, password: "", GID: -1, UserList: members, } } // NewSystemGroup adds a system group. func NewSystemGroup(name string, members ...string) *Group { return &Group{ Name: name, password: "", GID: -1, UserList: members, addSystemGroup: true, } } func (g *Group) filename() string { return fileGroup } // IsOfSystem indicates whether it is a system group. func (g *Group) IsOfSystem() bool { //loadConfig() if g.GID > config.login.SYS_GID_MIN && g.GID < config.login.SYS_GID_MAX { return true } return false } func (g *Group) String() string { return fmt.Sprintf("%s:%s:%d:%s\n", g.Name, g.password, g.GID, strings.Join(g.UserList, ",")) } // parseGroup parses the row of a group. func parseGroup(row string) (*Group, error) { fields := strings.Split(row, ":") if len(fields) != 4 { return nil, rowError{fileGroup, row} } gid, err := strconv.Atoi(fields[2]) if err != nil { return nil, atoiError{fileGroup, row, "GID"} } return &Group{ Name: fields[0], password: fields[1], GID: gid, UserList: strings.Split(fields[3], ","), }, nil } // == Lookup // // lookUp parses the group line searching a value into the field. // Returns nil if it is not found. func (*Group) lookUp(line string, f field, value interface{}) interface{} { _field := f.(groupField) allField := strings.Split(line, ":") arrayField := make(map[int][]string) intField := make(map[int]int) arrayField[3] = strings.Split(allField[3], ",") // Check integers var err error if intField[2], err = strconv.Atoi(allField[2]); err != nil { panic(atoiError{fileGroup, line, "GID"}) } // Check fields var isField bool if G_NAME&_field != 0 && allField[0] == value.(string) { isField = true } else if G_PASSWD&_field != 0 && allField[1] == value.(string) { isField = true } else if G_GID&_field != 0 && intField[2] == value.(int) { isField = true } else if G_MEMBER&_field != 0 && checkGroup(arrayField[3], value.(string)) { isField = true } else if G_ALL&_field != 0 { isField = true } if isField { return &Group{ Name: allField[0], password: allField[1], GID: intField[2], UserList: arrayField[3], } } return nil } // LookupGID looks up a group by group ID. func LookupGID(gid int) (*Group, error) { entries, err := LookupInGroup(G_GID, gid, 1) if err != nil { return nil, err } return entries[0], err } // LookupGroup looks up a group by name. func LookupGroup(name string) (*Group, error) { entries, err := LookupInGroup(G_NAME, name, 1) if err != nil { return nil, err } return entries[0], err } // LookupInGroup looks up a group by the given values. // // The count determines the number of fields to return: // n > 0: at most n fields // n == 0: the result is nil (zero fields) // n < 0: all fields func LookupInGroup(field groupField, value interface{}, n int) ([]*Group, error) { iEntries, err := lookUp(&Group{}, field, value, n) if err != nil { return nil, err } // == Convert to type group valueSlice := reflect.ValueOf(iEntries) entries := make([]*Group, valueSlice.Len()) for i := 0; i < valueSlice.Len(); i++ { entries[i] = valueSlice.Index(i).Interface().(*Group) } return entries, err } // Getgroups returns a list of the numeric ids of groups that the caller // belongs to. func Getgroups() []int { user := GetUsername() list := make([]int, 0) // The user could have its own group. if g, err := LookupGroup(user); err == nil { list = append(list, g.GID) } groups, err := LookupInGroup(G_MEMBER, user, -1) if err != nil { if _, ok := err.(NoFoundError); !ok { panic(err) } } for _, v := range groups { list = append(list, v.GID) } return list } // GetgroupsName returns a list of the groups that the caller belongs to. func GetgroupsName() []string { user := GetUsername() list := make([]string, 0) // The user could have its own group. if _, err := LookupGroup(user); err == nil { list = append(list, user) } groups, err := LookupInGroup(G_MEMBER, user, -1) if err != nil { if _, ok := err.(NoFoundError); !ok { panic(err) } } for _, v := range groups { list = append(list, v.Name) } return list } // == Editing // // AddGroup adds a group. func AddGroup(name string, members ...string) (gid int, err error) { s := NewGShadow(name, members...) if err = s.Add(nil); err != nil { return } return NewGroup(name, members...).Add() } // AddSystemGroup adds a system group. func AddSystemGroup(name string, members ...string) (gid int, err error) { s := NewGShadow(name, members...) if err = s.Add(nil); err != nil { return } return NewSystemGroup(name, members...).Add() } // Add adds a new group. // Whether GID is < 0, it will choose the first id available in the range set // in the system configuration. func (g *Group) Add() (gid int, err error) { loadConfig() group, err := LookupGroup(g.Name) if err != nil { if _, ok := err.(NoFoundError); !ok { return 0, err } } if group != nil { return 0, ErrGroupExist } if g.Name == "" { return 0, RequiredError("Name") } var db *dbfile if g.GID < 0 { db, gid, err = nextGUID(g.addSystemGroup) if err != nil { db.close() return 0, err } g.GID = gid } else { db, err = openDBFile(fileGroup, os.O_WRONLY|os.O_APPEND) if err != nil { return } // Check if Id is unique. _, err = LookupGID(g.GID) if err == nil { return 0, IdUsedError(g.GID) } else if _, ok := err.(NoFoundError); !ok { return 0, err } } g.password = "x" _, err = db.file.WriteString(g.String()) err2 := db.close() if err2 != nil && err == nil { err = err2 } return } // DelGroup removes a group from the system. func DelGroup(name string) (err error) { err = del(name, &Group{}) if err == nil { err = del(name, &GShadow{}) } return } // AddUsersToGroup adds the members to a group. func AddUsersToGroup(name string, members ...string) error { if len(members) == 0 { return fmt.Errorf("no members to add") } for i, v := range members { if v == "" { return EmptyMemberError(fmt.Sprintf("members[%s]", strconv.Itoa(i))) } } // Group gr, err := LookupGroup(name) if err != nil { return err } if err = _addMembers(&gr.UserList, members...); err != nil { return err } // Shadow group sg, err := LookupGShadow(name) if err != nil { return err } if err = _addMembers(&sg.UserList, members...); err != nil { return err } // Editing if err = edit(name, gr); err != nil { return err } if err = edit(name, sg); err != nil { return err } return nil } func _addMembers(userList *[]string, members ...string) error { // Check if some member is already in the file. for _, u := range *userList { for _, m := range members { if u == m { return fmt.Errorf("user %q is already set", u) } } } if len(*userList) == 1 && (*userList)[0] == "" { *userList = members } else { *userList = append(*userList, members...) } return nil } // DelUsersInGroup removes the specific members from a group. func DelUsersInGroup(name string, members ...string) error { if len(members) == 0 { return ErrNoMembers } for i, v := range members { if v == "" { return EmptyMemberError(fmt.Sprintf("members[%s]", strconv.Itoa(i))) } } // Group gr, err := LookupGroup(name) if err != nil { return err } if err = _delMembers(&gr.UserList, members...); err != nil { return err } // Shadow group sg, err := LookupGShadow(name) if err != nil { return err } if err = _delMembers(&sg.UserList, members...); err != nil { return err } // Editing if err = edit(name, gr); err != nil { return err } if err = edit(name, sg); err != nil { return err } return nil } func _delMembers(userList *[]string, members ...string) error { if len(*userList) == 1 && (*userList)[0] == "" { return ErrNoMembers } newUserList := make([]string, 0) for _, u := range *userList { found := false for _, m := range members { if u == m { found = true break } } if !found { newUserList = append(newUserList, u) } } if len(newUserList) == len(*userList) { return ErrNoMembers } *userList = make([]string, len(newUserList)) for i, v := range newUserList { (*userList)[i] = v } return nil } // == Utility // // checkGroup indicates if a value is into a group. func checkGroup(group []string, value string) bool { for _, v := range group { if v == value { return true } } return false } // == Errors // var ErrNoMembers = errors.New("no members to remove") // EmptyMemberError reports an empty member. type EmptyMemberError string func (e EmptyMemberError) Error() string { return "empty field: " + string(e) } osutil-1.5.0/user/group_test.go000066400000000000000000000110361462760513500165460ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "bufio" "io" "os" "testing" ) func TestGroupParser(t *testing.T) { f, err := os.Open(fileGroup) if err != nil { t.Fatal(err) } defer f.Close() buf := bufio.NewReader(f) for { line, _, err := buf.ReadLine() if err != nil { if err == io.EOF { break } t.Error(err) continue } if _, err = parseGroup(string(line)); err != nil { t.Error(err) } } } func TestGroupFull(t *testing.T) { entry, err := LookupGID(os.Getgid()) if err != nil || entry == nil { t.Error(err) } entry, err = LookupGroup("root") if err != nil || entry == nil { t.Error(err) } entries, err := LookupInGroup(G_MEMBER, "", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInGroup(G_ALL, nil, -1) if err != nil || len(entries) == 0 { t.Error(err) } } func TestGroupCount(t *testing.T) { count := 5 entries, err := LookupInGroup(G_ALL, nil, count) if err != nil || len(entries) != count { t.Error(err) } } func TestGroupError(t *testing.T) { _, err := LookupGroup("!!!???") if _, ok := err.(NoFoundError); !ok { t.Error("expected to report NoFoundError") } if _, err = LookupInGroup(G_MEMBER, "", 0); err != errSearch { t.Error("expected to report errSearch") } g := &Group{} if _, err = g.Add(); err != RequiredError("Name") { t.Error("expected to report RequiredError") } } func TestGetGroups(t *testing.T) { gids := Getgroups() gnames := GetgroupsName() for i, gid := range gids { g, err := LookupGID(gid) if err != nil { t.Error(err) } if g.Name != gnames[i] { t.Errorf("expected to match GID and group name") } } } func TestGroup_Add(t *testing.T) { group := NewGroup(GROUP, MEMBERS...) testGroupAdd(t, group, MEMBERS, false) group = NewSystemGroup(SYS_GROUP, MEMBERS...) testGroupAdd(t, group, MEMBERS, true) } func testGroupAdd(t *testing.T, group *Group, members []string, ofSystem bool) { prefix := "group" if ofSystem { prefix = "system " + prefix } id, err := group.Add() if err != nil { t.Fatal(err) } if id == -1 { t.Errorf("%s: got UID = -1", prefix) } if _, err = group.Add(); err == nil { t.Fatalf("%s: an existent group can not be added again", prefix) } else { if !IsExist(err) { t.Errorf("%s: expected to report ErrExist", prefix) } } if ofSystem { if !group.IsOfSystem() { t.Errorf("%s: IsOfSystem(): expected true", prefix) } } else { if group.IsOfSystem() { t.Errorf("%s: IsOfSystem(): expected false", prefix) } } // Check value stored name := "" if ofSystem { name = SYS_GROUP } else { name = GROUP } g, err := LookupGroup(name) if err != nil { t.Fatalf("%s: ", err) } if g.Name != name { t.Errorf("%s: expected to get name %q", prefix, name) } if g.UserList[0] != members[0] || g.UserList[1] != members[1] { t.Errorf("%s: expected to get members: %s", prefix, g.UserList) } } func TestGroup_Members(t *testing.T) { group := "g1" member := "m0" _, err := AddGroup(group, MEMBERS...) if err != nil { t.Fatal(err) } g_first, err := LookupGroup(group) if err != nil { t.Fatal(err) } sg_first, err := LookupGShadow(group) if err != nil { t.Fatal(err) } err = AddUsersToGroup(group, member) if err != nil { t.Fatal(err) } g_last, err := LookupGroup(group) if err != nil { t.Fatal(err) } sg_last, err := LookupGShadow(group) if err != nil { t.Fatal(err) } if len(g_first.UserList) == len(g_last.UserList) || g_last.UserList[0] != USER || g_last.UserList[1] != SYS_USER || g_last.UserList[2] != member { t.Error("group file: expected to add users into a group") } if len(sg_first.UserList) == len(sg_last.UserList) || sg_last.UserList[0] != USER || sg_last.UserList[1] != SYS_USER || sg_last.UserList[2] != member { t.Error("gshadow file: expected to add users into a group") } // == Delete err = DelUsersInGroup(group, member, USER) if err != nil { t.Fatal(err) } g_del, err := LookupGroup(group) if err != nil { t.Fatal(err) } sg_del, err := LookupGShadow(group) if err != nil { t.Fatal(err) } if len(g_del.UserList) == len(g_last.UserList) || g_del.UserList[0] != SYS_USER { t.Error("group file: expected to remove members of a group") } if len(sg_del.UserList) == len(sg_last.UserList) || sg_del.UserList[0] != SYS_USER { t.Error("gshadow file: expected to remove members of a group") } } osutil-1.5.0/user/gshadow.go000066400000000000000000000127361462760513500160170ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "fmt" "os" "reflect" "strings" ) type gshadowField int // Field names for shadowed group database. const ( GS_NAME gshadowField = 1 << iota GS_PASSWD GS_ADMIN GS_MEMBER GS_ALL ) func (f gshadowField) String() string { switch f { case GS_NAME: return "Name" case GS_PASSWD: return "Passwd" case GS_ADMIN: return "Admin" case GS_MEMBER: return "Member" } return "ALL" } // A GShadow represents the format of the shadowed information for a group account. type GShadow struct { // Group name. (Unique) // // It must be a valid group name, which exist on the system. Name string // Hashed password // // If the password field contains some string that is not a valid result of // crypt, for instance "!" or "*", users will not be able to use a unix // password to access the group (but group members do not need the password). // // The password is used when an user who is not a member of the group wants // to gain the permissions of this group (see "newgrp(1)"). // // This field may be empty, in which case only the group members can gain // the group permissions. // // A password field which starts with a exclamation mark means that the // password is locked. The remaining characters on the line represent the // password field before the password was locked. // // This password supersedes any password specified in '/etc/group'. password string // Group administrator list // // It must be a comma-separated list of user names. // // Administrators can change the password or the members of the group. // Administrators also have the same permissions as the members (see below). AdminList []string // Group member list // // It must be a comma-separated list of user names. // // Members can access the group without being prompted for a password. // You should use the same list of users as in /etc/group. UserList []string } // NewGShadow returns a new GShadow. func NewGShadow(username string, members ...string) *GShadow { return &GShadow{ Name: username, UserList: members, } } func (gs *GShadow) filename() string { return fileGShadow } func (gs *GShadow) String() string { return fmt.Sprintf("%s:%s:%s:%s\n", gs.Name, gs.password, strings.Join(gs.AdminList, ","), strings.Join(gs.UserList, ",")) } // parseGShadow parses the row of a group shadow. func parseGShadow(row string) (*GShadow, error) { fields := strings.Split(row, ":") if len(fields) != 4 { return nil, rowError{fileGShadow, row} } return &GShadow{ fields[0], fields[1], strings.Split(fields[2], ","), strings.Split(fields[3], ","), }, nil } // == Lookup // // lookUp parses the shadowed group line searching a value into the field. // Returns nil if it isn't found. func (*GShadow) lookUp(line string, f field, value interface{}) interface{} { _field := f.(gshadowField) _value := value.(string) allField := strings.Split(line, ":") arrayField := make(map[int][]string) arrayField[2] = strings.Split(allField[2], ",") arrayField[3] = strings.Split(allField[3], ",") // Check fields var isField bool if GS_NAME&_field != 0 && allField[0] == _value { isField = true } else if GS_PASSWD&_field != 0 && allField[1] == _value { isField = true } else if GS_ADMIN&_field != 0 && checkGroup(arrayField[2], _value) { isField = true } else if GS_MEMBER&_field != 0 && checkGroup(arrayField[3], _value) { isField = true } else if GS_ALL&_field != 0 { isField = true } if isField { return &GShadow{ allField[0], allField[1], arrayField[2], arrayField[3], } } return nil } // LookupGShadow looks up a shadowed group by name. func LookupGShadow(name string) (*GShadow, error) { entries, err := LookupInGShadow(GS_NAME, name, 1) if err != nil { return nil, err } return entries[0], err } // LookupInGShadow looks up a shadowed group by the given values. // // The count determines the number of fields to return: // n > 0: at most n fields // n == 0: the result is nil (zero fields) // n < 0: all fields func LookupInGShadow(field gshadowField, value string, n int) ([]*GShadow, error) { checkRoot() iEntries, err := lookUp(&GShadow{}, field, value, n) if err != nil { return nil, err } // == Convert to type GShadow valueSlice := reflect.ValueOf(iEntries) entries := make([]*GShadow, valueSlice.Len()) for i := 0; i < valueSlice.Len(); i++ { entries[i] = valueSlice.Index(i).Interface().(*GShadow) } return entries, err } // == Editing // // Add adds a new shadowed group. // If the key is not nil, generates a hashed password. // // It is created a backup before of modify the original file. func (gs *GShadow) Add(key []byte) (err error) { loadConfig() gshadow, err := LookupGShadow(gs.Name) if err != nil { if _, ok := err.(NoFoundError); !ok { return } } if gshadow != nil { return ErrGroupExist } if gs.Name == "" { return RequiredError("Name") } // Backup if err = backup(fileGShadow); err != nil { return } db, err := openDBFile(fileGShadow, os.O_WRONLY|os.O_APPEND) if err != nil { return } defer func() { e := db.close() if e != nil && err == nil { err = e } }() if key != nil { gs.password, _ = config.crypter.Generate(key, nil) } else { gs.password = "*" // Password disabled. } _, err = db.file.WriteString(gs.String()) return } osutil-1.5.0/user/gshadow_test.go000066400000000000000000000051721462760513500170520ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "bufio" "io" "os" "testing" ) func TestGShadowParser(t *testing.T) { f, err := os.Open(fileGShadow) if err != nil { t.Fatal(err) } defer f.Close() buf := bufio.NewReader(f) for { line, _, err := buf.ReadLine() if err != nil { if err == io.EOF { break } t.Error(err) continue } if _, err = parseGShadow(string(line)); err != nil { t.Error(err) } } } func TestGShadowFull(t *testing.T) { entry, err := LookupGShadow("root") if err != nil || entry == nil { t.Error(err) } entries, err := LookupInGShadow(GS_PASSWD, "!", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInGShadow(GS_ALL, "", -1) if err != nil || len(entries) == 0 { t.Error(err) } } func TestGShadowCount(t *testing.T) { count := 5 entries, err := LookupInGShadow(GS_ALL, "", count) if err != nil || len(entries) != count { t.Error(err) } } func TestGShadowError(t *testing.T) { _, err := LookupGShadow("!!!???") if _, ok := err.(NoFoundError); !ok { t.Error("expected to report NoFoundError") } if _, err = LookupInGShadow(GS_MEMBER, "", 0); err != errSearch { t.Error("expected to report errSearch") } gs := &GShadow{} if err = gs.Add(nil); err != RequiredError("Name") { t.Error("expected to report RequiredError") } } func TestGShadow_Add(t *testing.T) { shadow := NewGShadow(GROUP, MEMBERS...) err := shadow.Add(nil) if err != nil { t.Fatal(err) } if err = shadow.Add(nil); err == nil { t.Fatal("a shadowed group existent can not be added again") } else { if !IsExist(err) { t.Error("shadowed group: expected to report ErrExist") } } s, err := LookupGShadow(GROUP) if err != nil { t.Fatal(err) } if s.Name != GROUP { t.Errorf("shadowed group: expected to get name %q", GROUP) } } var ( groupKey1 = []byte("abc") groupKey2 = []byte("def") ) func TestGShadowCrypt(t *testing.T) { gs, err := LookupGShadow(GROUP) if err != nil { t.Fatal(err) } gs.Passwd(groupKey1) if err = config.crypter.Verify(gs.password, groupKey1); err != nil { t.Fatalf("expected to get the same hashed password for %q", groupKey1) } if err = ChGPasswd(GROUP, groupKey2); err != nil { t.Fatalf("expected to change password: %s", err) } gs, _ = LookupGShadow(GROUP) if err = config.crypter.Verify(gs.password, groupKey2); err != nil { t.Fatalf("ChGPasswd: expected to get the same hashed password for %q", groupKey2) } } osutil-1.5.0/user/id.go000066400000000000000000000066671462760513500147650ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "io" "os" "strconv" ) // nextUID returns the next free user id to use, according to whether it is a // system's user. func nextUID(isSystem bool) (db *dbfile, uid int, err error) { loadConfig() db, err = openDBFile(fileUser, os.O_RDWR) if err != nil { return } // Seek to file half size. info, err := db.file.Stat() if err != nil { db.close() return nil, 0, err } if _, err = db.file.Seek(info.Size()/2, io.SeekStart); err != nil { db.close() return nil, 0, err } // To starting to read from a new line if _, _, err = db.rd.ReadLine(); err != nil { db.close() return nil, 0, err } var minUid, maxUid int if isSystem { minUid, maxUid = config.login.SYS_UID_MIN, config.login.SYS_UID_MAX } else { minUid, maxUid = config.login.UID_MIN, config.login.UID_MAX } for { line, _, err := db.rd.ReadLine() if err == io.EOF { break } u, err := parseUser(string(line)) if err != nil { db.close() return nil, 0, err } if u.UID >= minUid && u.UID <= maxUid { uid = u.UID } } uid++ if uid == maxUid { return nil, 0, &IdRangeError{maxUid, isSystem, true} } return } // nextGUID returns the next free group id to use, according to whether it is a // system's group. func nextGUID(isSystem bool) (db *dbfile, gid int, err error) { loadConfig() db, err = openDBFile(fileGroup, os.O_RDWR) if err != nil { return } // Seek to file half size. info, err := db.file.Stat() if err != nil { db.close() return nil, 0, err } if _, err = db.file.Seek(info.Size()/2, io.SeekStart); err != nil { db.close() return nil, 0, err } // To starting to read from a new line if _, _, err = db.rd.ReadLine(); err != nil { db.close() return nil, 0, err } var minGid, maxGid int if isSystem { minGid, maxGid = config.login.SYS_GID_MIN, config.login.SYS_GID_MAX } else { minGid, maxGid = config.login.GID_MIN, config.login.GID_MAX } for { line, _, err := db.rd.ReadLine() if err == io.EOF { break } gr, err := parseGroup(string(line)) if err != nil { db.close() return nil, 0, err } if gr.GID >= minGid && gr.GID <= maxGid { gid = gr.GID } } gid++ if gid == maxGid { return nil, 0, &IdRangeError{maxGid, isSystem, false} } return } // NextSystemUID returns the next free system user id to use. func NextSystemUID() (int, error) { db, uid, err := nextUID(true) db.close() return uid, err } // NextSystemGID returns the next free system group id to use. func NextSystemGID() (int, error) { db, gid, err := nextGUID(true) db.close() return gid, err } // NextUID returns the next free user id to use. func NextUID() (int, error) { db, uid, err := nextUID(false) db.close() return uid, err } // NextGID returns the next free group id to use. func NextGID() (int, error) { db, gid, err := nextGUID(false) db.close() return gid, err } // * * * // IdRangeError records an error during the search for a free id to use. type IdRangeError struct { LastId int IsSystem bool IsUser bool } func (e *IdRangeError) Error() string { str := "" if e.IsSystem { str = "system " } if e.IsUser { str += "user: " } else { str += "group: " } str += strconv.Itoa(e.LastId) return "reached maximum identifier in " + str } osutil-1.5.0/user/id_test.go000066400000000000000000000016071462760513500160110ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "fmt" "testing" ) // TestAddGroup add a new group, so the next GIDs will have a value greater than // in the systems file. func TestID(t *testing.T) { id, err := NextSystemUID() if err != nil { t.Error(err) } if testing.Verbose() { fmt.Print(" Next system UID: ", id) } if id, err = NextUID(); err != nil { t.Error(err) } if testing.Verbose() { fmt.Println("\tNext UID:", id) } if id, err = NextSystemGID(); err != nil { t.Error(err) } if testing.Verbose() { fmt.Print(" Next system GID: ", id) } if id, err = NextGID(); err != nil { t.Error(err) } if testing.Verbose() { fmt.Println("\tNext GID:", id) } } osutil-1.5.0/user/shadow.go000066400000000000000000000305311462760513500156410ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "fmt" "os" "reflect" "strconv" "strings" "time" ) type shadowField int // Field names for shadowed password database. const ( S_NAME shadowField = 1 << iota S_PASSWD S_CHANGED S_MIN S_MAX S_WARN S_INACTIVE S_EXPIRE S_FLAG S_ALL ) func (f shadowField) String() string { switch f { case S_NAME: return "Name" case S_PASSWD: return "Passwd" case S_CHANGED: return "Changed" case S_MIN: return "Min" case S_MAX: return "Max" case S_WARN: return "Warn" case S_INACTIVE: return "Inactive" case S_EXPIRE: return "Expire" case S_FLAG: return "Flag" } return "ALL" } // changeType represents the options for last password change: // // < 0: disable aging // 0: change password // 1: enable aging // > 1: number of days type changeType int const ( _DISABLE_AGING changeType = -1 + iota _CHANGE_PASSWORD _ENABLE_AGING ) func (c changeType) String() string { if c == _DISABLE_AGING { return "" } return strconv.Itoa(int(c)) } func parseChange(s string) (changeType, error) { if s == "" { return _DISABLE_AGING, nil } i, err := strconv.Atoi(s) return changeType(i), err } // A Shadow represents the format of the information for a system's account and // optional aging information. // // The fields "changed" and "expire" deal with days from Jan 1, 1970; but since // package "time" deals with seconds, there is to divide it between the seconds // that a day has (24*60*60) which is done by functions "setChange" and // "SetExpire". // // To simulate an empty field in numeric fields, it is used a negative value. type Shadow struct { // Login name. (Unique) // // It must be a valid account name, which exist on the system. Name string // Hashed password // // If the password field contains some string that is not a valid result of // crypt, for instance "!" or "*", the user will not be able to use a unix // password to log in (but the user may log in the system by other means). // // This field may be empty, in which case no passwords are required to // authenticate as the specified login name. However, some applications // which read the '/etc/shadow' file may decide not to permit any access at // all if the password field is empty. // // A password field which starts with a exclamation mark means that the // password is locked. The remaining characters on the line represent the // password field before the password was locked. password string // Date of last password change // // The date of the last password change, expressed as the number of days // since Jan 1, 1970. // // The value 0 has a special meaning, which is that the user should change // her pasword the next time he will log in the system. // // An empty field means that password aging features are disabled. changed changeType // Minimum password age // // The minimum password age is the number of days the user will have to wait // before he will be allowed to change her password again. // // An empty field and value 0 mean that there are no minimum password age. Min int // Maximum password age // // The maximum password age is the number of days after which the user will // have to change her password. // // After this number of days is elapsed, the password may still be valid. // The user should be asked to change her password the next time he will // log in. // // An empty field means that there are no maximum password age, no password // warning period, and no password inactivity period (see below). // // If the maximum password age is lower than the minimum password age, the // user cannot change her password. Max int // Password warning period // // The number of days before a password is going to expire (see the maximum // password age above) during which the user should be warned. // // An empty field and value 0 mean that there are no password warning period. Warn int // Password inactivity period // // The number of days after a password has expired (see the maximum password // age above) during which the password should still be accepted (and the // user should update her password during the next login). // // After expiration of the password and this expiration period is elapsed, // no login is possible using the current user's password. // The user should contact her administrator. // // An empty field means that there are no enforcement of an inactivity period. Inactive int // Account expiration date // // The date of expiration of the account, expressed as the number of days // since Jan 1, 1970. // // Note that an account expiration differs from a password expiration. In // case of an acount expiration, the user shall not be allowed to login. In // case of a password expiration, the user is not allowed to login using her // password. // // An empty field means that the account will never expire. // // The value 0 should not be used as it is interpreted as either an account // with no expiration, or as an expiration on Jan 1, 1970. expire int // Reserved field // // This field is reserved for future use. flag int } // NewShadow returns a structure Shadow with fields "Min", "Max" and "Warn" // got from the system configuration, and enabling the features of password aging. func NewShadow(username string) *Shadow { loadConfig() return &Shadow{ Name: username, changed: _ENABLE_AGING, Min: config.login.PASS_MIN_DAYS, Max: config.login.PASS_MAX_DAYS, Warn: config.login.PASS_WARN_AGE, } } // setChange sets the date of the last password change to the current one. func (s *Shadow) setChange() { s.changed = changeType(secToDay(time.Now().Unix())) } // SetChangePasswd sets the account for that the user change her pasword the // next time he will log in the system. func (s *Shadow) SetChangePasswd() { s.changed = _CHANGE_PASSWORD } // DisableAging disables the features of password aging. func (s *Shadow) DisableAging() { s.changed = _DISABLE_AGING } // EnableAging enables the features of password aging. func (s *Shadow) EnableAging() { s.setChange() } // SetExpire sets the date of expiration of the account. func (s *Shadow) SetExpire(t *time.Time) { s.expire = secToDay(t.Unix()) } func (s *Shadow) filename() string { return fileShadow } func (s *Shadow) String() string { var min, max, warn, inactive, expire, flag string // Optional fields if s.Min != 0 { min = strconv.Itoa(s.Min) } if s.Max != 0 { max = strconv.Itoa(s.Max) } if s.Warn != 0 { warn = strconv.Itoa(s.Warn) } if s.Inactive != 0 { inactive = strconv.Itoa(s.Inactive) } if s.expire != 0 { expire = strconv.Itoa(s.expire) } if s.flag != 0 { flag = strconv.Itoa(s.flag) } return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s:%s\n", s.Name, s.password, s.changed, min, max, warn, inactive, expire, flag) } // parseShadow parses the row of a shadowed password. func parseShadow(row string) (*Shadow, error) { fields := strings.Split(row, ":") if len(fields) != 9 { return nil, rowError{fileShadow, row} } var min, max, warn, inactive, expire, flag int changed, err := parseChange(fields[2]) if err != nil { return nil, atoiError{fileShadow, row, "changed"} } // Optional fields if fields[3] != "" { if min, err = strconv.Atoi(fields[3]); err != nil { return nil, atoiError{fileShadow, row, "Min"} } } if fields[4] != "" { if max, err = strconv.Atoi(fields[4]); err != nil { return nil, atoiError{fileShadow, row, "Max"} } } if fields[5] != "" { if warn, err = strconv.Atoi(fields[5]); err != nil { return nil, atoiError{fileShadow, row, "Warn"} } } if fields[6] != "" { if inactive, err = strconv.Atoi(fields[6]); err != nil { return nil, atoiError{fileShadow, row, "Inactive"} } } if fields[7] != "" { if expire, err = strconv.Atoi(fields[7]); err != nil { return nil, atoiError{fileShadow, row, "expire"} } } if fields[8] != "" { if flag, err = strconv.Atoi(fields[8]); err != nil { return nil, atoiError{fileShadow, row, "flag"} } } return &Shadow{ fields[0], fields[1], changed, min, max, warn, inactive, expire, flag, }, nil } // == Lookup // // lookUp parses the shadow passwd line searching a value into the field. // Returns nil if is not found. func (*Shadow) lookUp(line string, f field, value interface{}) interface{} { _field := f.(shadowField) allField := strings.Split(line, ":") intField := make(map[int]int) // Check integers changed, err := parseChange(allField[2]) if err != nil { panic(atoiError{fileShadow, line, "changed"}) } // These fields could be empty. if allField[3] != "" { if intField[3], err = strconv.Atoi(allField[3]); err != nil { panic(atoiError{fileShadow, line, "Min"}) } } if allField[4] != "" { if intField[4], err = strconv.Atoi(allField[4]); err != nil { panic(atoiError{fileShadow, line, "Max"}) } } if allField[5] != "" { if intField[5], err = strconv.Atoi(allField[5]); err != nil { panic(atoiError{fileShadow, line, "Warn"}) } } if allField[6] != "" { if intField[6], err = strconv.Atoi(allField[6]); err != nil { panic(atoiError{fileShadow, line, "Inactive"}) } } if allField[7] != "" { if intField[7], err = strconv.Atoi(allField[7]); err != nil { panic(atoiError{fileShadow, line, "expire"}) } } if allField[8] != "" { if intField[8], err = strconv.Atoi(allField[8]); err != nil { panic(atoiError{fileShadow, line, "flag"}) } } // Check fields var isField bool if S_NAME&_field != 0 && allField[0] == value.(string) { isField = true } else if S_PASSWD&_field != 0 && allField[1] == value.(string) { isField = true } else if S_CHANGED&_field != 0 && int(changed) == value.(int) { isField = true } else if S_MIN&_field != 0 && intField[3] == value.(int) { isField = true } else if S_MAX&_field != 0 && intField[4] == value.(int) { isField = true } else if S_WARN&_field != 0 && intField[5] == value.(int) { isField = true } else if S_INACTIVE&_field != 0 && intField[6] == value.(int) { isField = true } else if S_EXPIRE&_field != 0 && intField[7] == value.(int) { isField = true } else if S_FLAG&_field != 0 && intField[8] == value.(int) { isField = true } else if S_ALL&_field != 0 { isField = true } if isField { return &Shadow{ allField[0], allField[1], changed, intField[3], intField[4], intField[5], intField[6], intField[7], intField[8], } } return nil } // LookupShadow looks for the entry for the given user name. func LookupShadow(name string) (*Shadow, error) { entries, err := LookupInShadow(S_NAME, name, 1) if err != nil { return nil, err } return entries[0], err } // LookupInShadow looks up a shadowed password by the given values. // // The count determines the number of fields to return: // n > 0: at most n fields // n == 0: the result is nil (zero fields) // n < 0: all fields func LookupInShadow(field shadowField, value interface{}, n int) ([]*Shadow, error) { checkRoot() iEntries, err := lookUp(&Shadow{}, field, value, n) if err != nil { return nil, err } // == Convert to type shadow valueSlice := reflect.ValueOf(iEntries) entries := make([]*Shadow, valueSlice.Len()) for i := 0; i < valueSlice.Len(); i++ { entries[i] = valueSlice.Index(i).Interface().(*Shadow) } return entries, err } // == Editing // // Add adds a new shadowed user. // If the key is not nil, generates a hashed password. // // It is created a backup before of modify the original file. func (s *Shadow) Add(key []byte) (err error) { loadConfig() shadow, err := LookupShadow(s.Name) if err != nil { if _, ok := err.(NoFoundError); !ok { return } } if shadow != nil { return ErrUserExist } if s.Name == "" { return RequiredError("Name") } if s.Max == 0 { return RequiredError("Max") } if s.Warn == 0 { return RequiredError("Warn") } // Backup if err = backup(fileShadow); err != nil { return } db, err := openDBFile(fileShadow, os.O_WRONLY|os.O_APPEND) if err != nil { return } defer func() { e := db.close() if e != nil && err == nil { err = e } }() if key != nil { s.password, _ = config.crypter.Generate(key, nil) if s.changed == _ENABLE_AGING { s.setChange() } } else { s.password = "*" // Password disabled. } _, err = db.file.WriteString(s.String()) return } osutil-1.5.0/user/shadow_test.go000066400000000000000000000052441462760513500167030ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "bufio" "io" "os" "testing" ) func TestShadowParser(t *testing.T) { f, err := os.Open(fileShadow) if err != nil { t.Fatal(err) } defer f.Close() buf := bufio.NewReader(f) for { line, _, err := buf.ReadLine() if err != nil { if err == io.EOF { break } t.Error(err) continue } if _, err = parseShadow(string(line)); err != nil { t.Error(err) } } } func TestShadowFull(t *testing.T) { entry, err := LookupShadow("root") if err != nil || entry == nil { t.Error(err) } entries, err := LookupInShadow(S_PASSWD, "!", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInShadow(S_ALL, nil, -1) if err != nil || len(entries) == 0 { t.Error(err) } } func TestShadowCount(t *testing.T) { count := 2 entries, err := LookupInShadow(S_MIN, 0, count) if err != nil || len(entries) != count { t.Error(err) } count = 5 entries, err = LookupInShadow(S_ALL, nil, count) if err != nil || len(entries) != count { t.Error(err) } } func TestShadowError(t *testing.T) { _, err := LookupShadow("!!!???") if _, ok := err.(NoFoundError); !ok { t.Error("expected to report NoFoundError") } if _, err = LookupInShadow(S_MIN, 0, 0); err != errSearch { t.Error("expected to report errSearch") } s := &Shadow{} if err = s.Add(nil); err != RequiredError("Name") { t.Error("expected to report RequiredError") } } func TestShadow_Add(t *testing.T) { shadow := NewShadow(USER) err := shadow.Add(nil) if err != nil { t.Fatal(err) } if err = shadow.Add(nil); err == nil { t.Fatal("a shadowed user existent can not be added again") } else { if !IsExist(err) { t.Error("shadow: expected to report ErrExist") } } s, err := LookupShadow(USER) if err != nil { t.Fatal(err) } if s.Name != USER { t.Errorf("shadow: expected to get name %q", USER) } } var ( userKey1 = []byte("123") userKey2 = []byte("456") ) func TestShadowCrypt(t *testing.T) { s, err := LookupShadow(USER) if err != nil { t.Fatal(err) } s.Passwd(userKey1) if err = config.crypter.Verify(s.password, userKey1); err != nil { t.Fatalf("expected to get the same hashed password for %q", userKey1) } if err = ChPasswd(USER, userKey2); err != nil { t.Fatalf("expected to change password: %s", err) } s, _ = LookupShadow(USER) if err = config.crypter.Verify(s.password, userKey2); err != nil { t.Fatalf("ChPasswd: expected to get the same hashed password for %q", userKey2) } } osutil-1.5.0/user/user.go000066400000000000000000000214371462760513500153370ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "fmt" "os" "path" "reflect" "strconv" "strings" ) type userField int // Field names for user database. const ( U_NAME userField = 1 << iota U_PASSWD U_UID U_GID U_GECOS U_DIR U_SHELL U_ALL // To get lines without searching into a field. ) func (f userField) String() string { switch f { case U_NAME: return "Name" case U_PASSWD: return "Passwd" case U_UID: return "UID" case U_GID: return "GID" case U_GECOS: return "GECOS" case U_DIR: return "Dir" case U_SHELL: return "Shell" } return "ALL" } // An User represents an user account. type User struct { // Login name. (Unique) Name string // Optional hashed password // // The hashed password field may be blank, in which case no password is // required to authenticate as the specified login name. However, some // applications which read the '/etc/passwd' file may decide not to permit // any access at all if the password field is blank. If the password field // is a lower-case "x", then the encrypted password is actually stored in // the "shadow(5)" file instead; there must be a corresponding line in the // '/etc/shadow' file, or else the user account is invalid. If the password // field is any other string, then it will be treated as an hashed password, // as specified by "crypt(3)". password string // Numerical user ID. (Unique) UID int // Numerical group ID GID int // User name or comment field // // The comment field is used by various system utilities, such as "finger(1)". Gecos string // User home directory // // The home directory field provides the name of the initial working // directory. The login program uses this information to set the value of // the $HOME environmental variable. Dir string // Optional user command interpreter // // The command interpreter field provides the name of the user's command // language interpreter, or the name of the initial program to execute. // The login program uses this information to set the value of the "$SHELL" // environmental variable. If this field is empty, it defaults to the value // "/bin/sh". Shell string addSystemUser bool } // NewUser returns a new User with both fields "Dir" and "Shell" got from // the system configuration. func NewUser(name string, gid int) *User { loadConfig() return &User{ Name: name, Dir: path.Join(config.useradd.HOME, name), Shell: config.useradd.SHELL, UID: -1, GID: gid, } } // NewSystemUser returns a new system user. func NewSystemUser(name, homeDir string, gid int) *User { return &User{ Name: name, Dir: homeDir, Shell: "/bin/false", UID: -1, GID: gid, addSystemUser: true, } } func (u *User) filename() string { return fileUser } // IsOfSystem indicates whether it is a system user. func (u *User) IsOfSystem() bool { //loadConfig() if u.UID > config.login.SYS_UID_MIN && u.UID < config.login.SYS_UID_MAX { return true } return false } func (u *User) String() string { return fmt.Sprintf("%s:%s:%d:%d:%s:%s:%s\n", u.Name, u.password, u.UID, u.GID, u.Gecos, u.Dir, u.Shell) } // parseUser parses the row of an user. func parseUser(row string) (*User, error) { fields := strings.Split(row, ":") if len(fields) != 7 { return nil, rowError{fileUser, row} } uid, err := strconv.Atoi(fields[2]) if err != nil { return nil, atoiError{fileUser, row, "UID"} } gid, err := strconv.Atoi(fields[3]) if err != nil { return nil, atoiError{fileUser, row, "GID"} } return &User{ Name: fields[0], password: fields[1], UID: uid, GID: gid, Gecos: fields[4], Dir: fields[5], Shell: fields[6], }, nil } // == Lookup // // lookUp parses the user line searching a value into the field. // Returns nil if is not found. func (*User) lookUp(line string, f field, value interface{}) interface{} { _field := f.(userField) allField := strings.Split(line, ":") intField := make(map[int]int) // Check integers var err error if intField[2], err = strconv.Atoi(allField[2]); err != nil { panic(atoiError{fileUser, line, "UID"}) } if intField[3], err = strconv.Atoi(allField[3]); err != nil { panic(atoiError{fileUser, line, "GID"}) } // Check fields var isField bool if U_NAME&_field != 0 && allField[0] == value.(string) { isField = true } else if U_PASSWD&_field != 0 && allField[1] == value.(string) { isField = true } else if U_UID&_field != 0 && intField[2] == value.(int) { isField = true } else if U_GID&_field != 0 && intField[3] == value.(int) { isField = true } else if U_GECOS&_field != 0 && allField[4] == value.(string) { isField = true } else if U_DIR&_field != 0 && allField[5] == value.(string) { isField = true } else if U_SHELL&_field != 0 && allField[6] == value.(string) { isField = true } else if U_ALL&_field != 0 { isField = true } if isField { return &User{ Name: allField[0], password: allField[1], UID: intField[2], GID: intField[3], Gecos: allField[4], Dir: allField[5], Shell: allField[6], } } return nil } // LookupUID looks up an user by user ID. func LookupUID(uid int) (*User, error) { entries, err := LookupInUser(U_UID, uid, 1) if err != nil { return nil, err } return entries[0], err } // LookupUser looks up an user by name. func LookupUser(name string) (*User, error) { entries, err := LookupInUser(U_NAME, name, 1) if err != nil { return nil, err } return entries[0], err } // LookupInUser looks up an user by the given values. // // The count determines the number of fields to return: // n > 0: at most n fields // n == 0: the result is nil (zero fields) // n < 0: all fields func LookupInUser(field userField, value interface{}, n int) ([]*User, error) { iEntries, err := lookUp(&User{}, field, value, n) if err != nil { return nil, err } // == Convert to type user valueSlice := reflect.ValueOf(iEntries) entries := make([]*User, valueSlice.Len()) for i := 0; i < valueSlice.Len(); i++ { entries[i] = valueSlice.Index(i).Interface().(*User) } return entries, err } // GetUsername returns the user name from the password database for the actual // process. // It panics whther there is an error at searching the UID. func GetUsername() string { entry, err := LookupUID(os.Getuid()) if err != nil { panic(err) } return entry.Name } // GetUsernameFromEnv returns the user name from the environment variable // for the actual process. func GetUsernameFromEnv() string { userEnv := []string{"USER", "USERNAME", "LOGNAME", "LNAME"} for _, val := range userEnv { name := os.Getenv(val) if name != "" { return name } } return "" } // == Editing // // AddUser adds an user to both user and shadow files. func AddUser(name string, gid int) (uid int, err error) { s := NewShadow(name) if err = s.Add(nil); err != nil { return } return NewUser(name, gid).Add() } // AddSystemUser adds a system user to both user and shadow files. func AddSystemUser(name, homeDir string, gid int) (uid int, err error) { s := NewShadow(name) if err = s.Add(nil); err != nil { return } return NewSystemUser(name, homeDir, gid).Add() } // Add adds a new user. // Whether UID is < 0, it will choose the first id available in the range set // in the system configuration. func (u *User) Add() (uid int, err error) { loadConfig() user, err := LookupUser(u.Name) if err != nil { if _, ok := err.(NoFoundError); !ok { return } } if user != nil { return 0, ErrUserExist } if u.Name == "" { return 0, RequiredError("Name") } if u.Dir == "" { return 0, RequiredError("Dir") } if u.Dir == config.useradd.HOME { return 0, HomeError(config.useradd.HOME) } if u.Shell == "" { return 0, RequiredError("Shell") } var db *dbfile if u.UID < 0 { db, uid, err = nextUID(u.addSystemUser) if err != nil { db.close() return 0, err } u.UID = uid } else { db, err = openDBFile(fileUser, os.O_WRONLY|os.O_APPEND) if err != nil { return 0, err } // Check if Id is unique. _, err = LookupUID(u.UID) if err == nil { return 0, IdUsedError(u.UID) } else if _, ok := err.(NoFoundError); !ok { return 0, err } } u.password = "x" _, err = db.file.WriteString(u.String()) err2 := db.close() if err2 != nil && err == nil { err = err2 } return } // DelUser removes an user from the system. func DelUser(name string) (err error) { err = del(name, &User{}) if err == nil { err = del(name, &Shadow{}) } return } // == Errors // // A HomeError reports an error at adding an account with invalid home directory. type HomeError string func (e HomeError) Error() string { return "invalid directory for the home directory of an account: " + string(e) } osutil-1.5.0/user/user_test.go000066400000000000000000000073771462760513500164050ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "bufio" "io" "os" "testing" ) func TestUserParser(t *testing.T) { f, err := os.Open(fileUser) if err != nil { t.Fatal(err) } defer f.Close() buf := bufio.NewReader(f) for { line, _, err := buf.ReadLine() if err != nil { if err == io.EOF { break } t.Error(err) continue } if _, err = parseUser(string(line)); err != nil { t.Error(err) } } } func TestUserFull(t *testing.T) { entry, err := LookupUID(os.Getuid()) if err != nil || entry == nil { t.Error(err) } entry, err = LookupUser("root") if err != nil || entry == nil { t.Error(err) } entries, err := LookupInUser(U_GID, 65534, -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInUser(U_GECOS, "", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInUser(U_DIR, "/bin", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInUser(U_SHELL, "/bin/false", -1) if err != nil || entries == nil { t.Error(err) } entries, err = LookupInUser(U_ALL, nil, -1) if err != nil || len(entries) == 0 { t.Error(err) } } func TestUserCount(t *testing.T) { count := 2 entries, err := LookupInUser(U_SHELL, "/bin/false", count) if err != nil || len(entries) != count { t.Error(err) } count = 5 entries, err = LookupInUser(U_ALL, nil, count) if err != nil || len(entries) != count { t.Error(err) } } func TestUserError(t *testing.T) { _, err := LookupUser("!!!???") if _, ok := err.(NoFoundError); !ok { t.Error("expected to report NoFoundError") } if _, err = LookupInUser(U_SHELL, "/bin/false", 0); err != errSearch { t.Error("expected to report errSearch") } u := &User{} if _, err = u.Add(); err != RequiredError("Name") { t.Error("expected to report RequiredError") } u = &User{Name: USER, Dir: config.useradd.HOME, Shell: config.useradd.SHELL} if _, err = u.Add(); err != HomeError(config.useradd.HOME) { t.Error("expected to report HomeError") } } func TestUser_Add(t *testing.T) { user := NewUser(USER, GID) user.Dir = "/tmp" testUserAdd(t, user, false) user = NewSystemUser(SYS_USER, "/tmp", GID) testUserAdd(t, user, true) } func testUserAdd(t *testing.T, user *User, ofSystem bool) { prefix := "user" if ofSystem { prefix = "system " + prefix } id, err := user.Add() if err != nil { t.Fatal(err) } if id == -1 { t.Errorf("%s: got UID = -1", prefix) } if _, err = user.Add(); err == nil { t.Fatalf("%s: an existent user can not be added again", prefix) } else { if !IsExist(err) { t.Errorf("%s: expected to report ErrExist", prefix) } } if ofSystem { if !user.IsOfSystem() { t.Errorf("%s: IsOfSystem(): expected true", prefix) } } else { if user.IsOfSystem() { t.Errorf("%s: IsOfSystem(): expected false", prefix) } } // Check value stored name := "" if ofSystem { name = SYS_USER } else { name = USER } u, err := LookupUser(name) if err != nil { t.Fatalf("%s: ", err) } if u.Name != name { t.Errorf("%s: expected to get name %q", prefix, name) } } func TestUserLock(t *testing.T) { err := LockUser(USER) if err != nil { t.Fatal(err) } s, err := LookupShadow(USER) if err != nil { t.Fatal(err) } if s.password[0] != lockChar { t.Fatalf("expected to get password starting with '%c', got: '%c'", lockChar, s.password[0]) } err = UnlockUser(USER) if err != nil { t.Fatal(err) } s, err = LookupShadow(USER) if err != nil { t.Fatal(err) } if s.password[0] == lockChar { t.Fatalf("no expected to get password starting with '%c'", lockChar) } } osutil-1.5.0/user/util.go000066400000000000000000000015651462760513500153360ustar00rootroot00000000000000// Copyright 2010 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import "os" var isRoot bool func init() { // Root's user ID is 0. if os.Getuid() == 0 { isRoot = true } } // checkRoot checks if the user is root. func checkRoot() { if !isRoot { panic("you have to be Root") } } // exist checks if the file exists. func exist(file string) (bool, error) { _, err := os.Stat(file) if err != nil { if err == os.ErrNotExist { return false, nil } return false, err } return true, nil } // _SEC_PER_DAY is the number of secons that a day has. const _SEC_PER_DAY = 24 * 60 * 60 // secToDay converts from secons to days. func secToDay(sec int64) int { return int(sec / _SEC_PER_DAY) } osutil-1.5.0/user/util_test.go000066400000000000000000000012011462760513500163600ustar00rootroot00000000000000// Copyright 2013 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import ( "testing" "time" ) func TestSecToDays(t *testing.T) { now := time.Now() if now.Day() > 3 { before := time.Date(now.Year(), now.Month(), now.Day()-3, now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.Local) diff := secToDay(now.Unix()) - secToDay(before.Unix()) if diff != 3 { t.Fatalf("expected to get a difference of 3 days, got %d", diff) } } } osutil-1.5.0/user/z_test.go000066400000000000000000000017221462760513500156640ustar00rootroot00000000000000// Copyright 2012 Jonas mg // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package user import "testing" func TestDelUser(t *testing.T) { err := DelUser(USER) if err != nil { t.Fatal(err) } _, err = LookupUser(USER) if _, ok := err.(NoFoundError); !ok { t.Error("expected to get error NoFoundError") } _, err = LookupShadow(USER) if _, ok := err.(NoFoundError); !ok { t.Error("expected to get error NoFoundError") } } func TestDelGroup(t *testing.T) { err := DelGroup(GROUP) if err != nil { t.Fatal(err) } _, err = LookupGroup(GROUP) if _, ok := err.(NoFoundError); !ok { t.Error("expected to get error NoFoundError") } _, err = LookupGShadow(GROUP) if _, ok := err.(NoFoundError); !ok { t.Error("expected to get error NoFoundError") } } func TestZ(*testing.T) { removeTempFiles() }