pax_global_header00006660000000000000000000000064141116707370014521gustar00rootroot0000000000000052 comment=530b1d645e98cfe807269ff9ed1c27778a98ab90 libhosty-1.1.0/000077500000000000000000000000001411167073700133555ustar00rootroot00000000000000libhosty-1.1.0/.github/000077500000000000000000000000001411167073700147155ustar00rootroot00000000000000libhosty-1.1.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001411167073700171005ustar00rootroot00000000000000libhosty-1.1.0/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000011721411167073700215730ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: 'Issue: ' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Windows/Linux/macOS] **Additional context** Add any other context about the problem here. libhosty-1.1.0/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011231411167073700226220ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. libhosty-1.1.0/.github/workflows/000077500000000000000000000000001411167073700167525ustar00rootroot00000000000000libhosty-1.1.0/.github/workflows/build-and-test.yml000066400000000000000000000005611411167073700223130ustar00rootroot00000000000000name: Build and Test on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.15 - name: Build run: go build -v ./... - name: Test run: go test -v ./... libhosty-1.1.0/.github/workflows/go.yml000066400000000000000000000005611411167073700201040ustar00rootroot00000000000000name: Test and Build on: push: branches: [ main ] pull_request: branches: [ main ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.15 - name: Build run: go build -v ./... - name: Test run: go test -v ./... libhosty-1.1.0/.gitignore000066400000000000000000000001111411167073700153360ustar00rootroot00000000000000# test coverage output ./coverage.out ./cover.out coverage.out cover.out libhosty-1.1.0/LICENSE000066400000000000000000000261351411167073700143710ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. libhosty-1.1.0/README.md000066400000000000000000000115631411167073700146420ustar00rootroot00000000000000# libhosty [![made-with-Go](https://img.shields.io/badge/made%20with-Go-1f425f.svg)](http://golang.org) [![Go Report Card](https://goreportcard.com/badge/github.com/areYouLazy/libhosty)](https://goreportcard.com/report/github.com/areYouLazy/libhosty) [![Build and Test](https://github.com/areYouLazy/libhosty/actions/workflows/build-and-test.yml/badge.svg?branch=main&event=push)](https://github.com/areYouLazy/libhosty/actions/workflows/build-and-test.yml) ![gopherbadger-tag-do-not-edit](coverage_badge.png) [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/areYouLazy/libhosty) ## Description libhosty is a pure golang library to manipulate the hosts file. It is inspired by [txeh](https://github.com/txn2/txeh), with some enrichments. ## Table of Contents * [Main Features](#Main-Features) * [Installation](#Installation) * [Usage](#Usage) * [Contributing](#Contributing) * [Templates](#Templates) * [Credits](#Credits) * [License](#License) ## Main Features * Comment/Uncomment a line without removing it from the file * Restore the default hosts file for your system * Add/Remove Address lines * Add/Remove Comment lines * Add/Remove Empty lines * Query by hostname * Automatically handles duplicate entries ## Installation Ensure you have go on your system ```bash > go version go version go1.15.6 linux/amd64 ``` and pull the library ```bash > go get github.com/areYouLazy/libhosty ``` ## Usage To use the library, just import it and call the `Init()` method. Note: This code doesn't handle errors for readability purposes, but you SHOULD! ```go package main import "github.com/areYouLazy/libhosty" func main() { //you can define a custom config object // and use it to initialize libhosty with a custom hosts file // //cnf, _ := libhosty.NewHostsFileConfig("/home/sonica/hosts-export.txt") //hfl, _ := libhosty.InitWithConfig(cnf) //or initialize libhosty that will automatically try to loads // then default hosts file for your OS hfl, _ := libhosty.Init() //add an empty line hfl.AddEmptyFileLine() //add a host with a comment hfl.AddHostFileLine("12.12.12.12", "my.host.name", "comment on my hostname!") //add a comment hfl.AddCommentFileLine("just a comment") //add an empty line hfl.AddEmptyFileLine() //add another host without comment hfl.AddHostsFileLine("13.13.13.13", "another.host.name", "") //add another fqdn to the previous ip hfl.AddHostsFileLine("12.12.12.12", "second.host.name", "") // comment for host lines can be done by hostname, row line // or IP (as net.IP or string) // // Comment the line with address 12.12.12.12 // // By-Row-Number idx, _ := hfl.GetHostsFileLineByHostname("second.host.name") hfl.CommentHostsFileLineByRow(idx) // // By-Hostname hfl.CommentHostsFileLineByHostname("second.host.name") // // By-Address-As-IP ip := net.ParseIP("12.12.12.12") hfl.CommentHostsFileLineByIP(ip) // // By-Address-As-String hfl.CommentHostsFileLineByAddress("12.12.12.12") // render the hosts file fmt.Println(hfl.RenderHostsFile()) // write file to disk hfl.SaveHostsFile() // or to a custom location hfl.SaveHostsFileAs("/home/sonica/hosts-export.txt") // restore the original hosts file for linux hfl.RestoreDefaultLinuxHostsFile() // render the hosts file fmt.Println(hfl.RenderHostsFile()) // write to disk hfl.SaveHostsFile() } ``` The 1st `fmt.Println()` should output something like this (in a linux host) ```console # Do not remove the following line, or various programs # that require network functionality will fail. 127.0.0.1 localhost.localdomain localhost ::1 localhost6.localdomain6 localhost6 # 12.12.12.12 my.host.name second.host.name #comment on my hostname! # just a comment line 13.13.13.13 another.host.name ``` While the 2nd `fmt.Println()` should output the default template for linux systems ```console # Do not remove the following line, or various programs # that require network functionality will fail. 127.0.0.1 localhost.localdomain localhost ::1 localhost6.localdomain6 localhost6 ``` If you handle errors properly, you'll notice that this example program will fail on the `SaveHostsFile()` call if started as a normal user, as editing the hosts file requires root privileges. This does not prevent libhosty from loading, managing, rendering and exporting the hosts file ## Contributing Issues and PRs are more than welcome! ### Templates If you find a hosts template (like the Docker one) that you think can be useful to have in this library feel free to open a Pull Request ## Credits Project Contributors will be listed here ## License Licenses under Apache License 2.0 libhosty-1.1.0/TODO.md000066400000000000000000000000531411167073700144420ustar00rootroot00000000000000# TODO * Improve tests * Improve comments libhosty-1.1.0/coverage_badge.png000066400000000000000000000042321411167073700170010ustar00rootroot00000000000000PNG  IHDRt\n}sRGBTIDAThL_=s'qw@EQ9 A-)+ 1)Mc.Zm5L*MimD&+5keQh'PS`]'2-ܝp (u[?OyNj%IZXx{FO'4SB|.UTU=BLhQ3&Z; , D( 1,I8х,"0*I04PY"--(:;;9w~!^ɲLvv6iiif_NUU555k4T$-En"B1sUO闙9 !/3ɒ«sʙg^]s`T"y5_\% !kM6aXرcX,222X`imm%..A}}=n&Fg/IbͿDhJ8~l+#1/w많2ǰ& [~$I YKHCcbbXhχdPYY,zϟ ::^\ٳ… G`0p1 oSPP@\\?~K.PQQYjTVVrȑ`MMMt:磪*}yyy`BPf 2z۰$M1/2E;8Q6<0A|!w{y%{i^Df-^3Dx>}:mmmTUE$dY`0hd:t'|˗/S\\El6 ݎ$I!Xx19BO<](**BU`ZZ$6n@CC)))$''s4^m/ZȺ0\Fi:-]MnǦ;8pt/rrrB. +͛qQ|>lٲW_}ž}Bq\lذ+Wb6ikkZB!(,,dժUlܸ>X,lݺxYn]0w?%%%,].jjjlܹJJJPSNs=7nZ,;W1'K@J<Tp?Ŏ_3k 8FIqcoӛQuC*vEh4t: ,zq:Ʉ(ߋKQ ^~adYr|U@{{;# 2:OTT}}}\.z=̚5c8F34Y cCvxOG-5֤Ug)++k,^Ccc#nsAXب,=IENDB`libhosty-1.1.0/errors.go000066400000000000000000000026041411167073700152220ustar00rootroot00000000000000package libhosty import ( "errors" "fmt" ) //ErrNotAnAddressLine used when operating on a non-address line for operation // related to address lines, such as comment/uncomment var ErrNotAnAddressLine = errors.New("this line is not of type ADDRESS") //ErrUncommentableLine used when try to comment a line that cannot be commented var ErrUncommentableLine = errors.New("this line cannot be commented") //ErrAlredyCommentedLine used when try to comment an alredy commented line var ErrAlredyCommentedLine = errors.New("this line is alredy commented") //ErrAlredyUncommentedLine used when try to uncomment an alredy uncommented line var ErrAlredyUncommentedLine = errors.New("this line is alredy uncommented") //ErrAddressNotFound used when provided address is not found var ErrAddressNotFound = errors.New("cannot find a line with given address") //ErrHostnameNotFound used when provided hostname is not found var ErrHostnameNotFound = errors.New("cannot find a line with given hostname") //ErrUnknown used when we don't know what's happened var ErrUnknown = errors.New("unknown error") //ErrCannotParseIPAddress used when unable to parse given ip address func ErrCannotParseIPAddress(ip string) error { return fmt.Errorf("cannot parse IP Address: %s", ip) } //ErrUnrecognizedOS used when unable to recognize OS func ErrUnrecognizedOS(os string) error { return fmt.Errorf("unrecognized OS: %s", os) } libhosty-1.1.0/formatter.go000066400000000000000000000015751411167073700157170ustar00rootroot00000000000000package libhosty import ( "fmt" "strings" ) // lineFormatter return a readable form for the given HostsFileLine object func lineFormatter(hfl HostsFileLine) string { // returns raw if we don't need to edit the line // this is for UNKNOWN, EMPTY and COMMENT linetypes if hfl.Type < LineTypeAddress { return hfl.Raw } // check if it's a commented line if hfl.IsCommented { // check if there's a comment for that line if len(hfl.Comment) > 0 { return fmt.Sprintf("# %-16s %s #%s", hfl.Address, strings.Join(hfl.Hostnames, " "), hfl.Comment) } return fmt.Sprintf("# %-16s %s", hfl.Address, strings.Join(hfl.Hostnames, " ")) } // return the actual hosts entry if len(hfl.Comment) > 0 { return fmt.Sprintf("%-16s %s #%s", hfl.Address, strings.Join(hfl.Hostnames, " "), hfl.Comment) } return fmt.Sprintf("%-16s %s", hfl.Address, strings.Join(hfl.Hostnames, " ")) } libhosty-1.1.0/formatter_test.go000066400000000000000000000035101411167073700167450ustar00rootroot00000000000000package libhosty import ( "net" "strings" "testing" ) func TestLineFormatter(t *testing.T) { // define custom hostsFileLine with IsCommented hfl := HostsFileLine{ Number: 0, Type: 30, Address: net.ParseIP("1.1.1.1"), Parts: []string{""}, Hostnames: []string{"my.host.name"}, Raw: "1.1.1.1 my.host.name # This is a host", Comment: "This is a host", IsCommented: true, trimed: "1.1.1.1 my.host.name", } // invoke lineFormatter hosts file line l := lineFormatter(hfl) // define what we expect w := "# 1.1.1.1 my.host.name #This is a host" // check if !strings.EqualFold(l, w) { t.Fatalf(`IsCommented=true and Comment: wants '%q' got '%q'`, w, l) } // test without IsCommented hfl.IsCommented = false l = lineFormatter(hfl) w = "1.1.1.1 my.host.name #This is a host" if !strings.EqualFold(l, w) { t.Fatalf(`IsCommented=false and Comment: wants '%q' got '%q'`, w, l) } // test with IsCommented but without comment in line hfl.IsCommented = true hfl.Comment = "" l = lineFormatter(hfl) w = "# 1.1.1.1 my.host.name" if !strings.EqualFold(l, w) { t.Fatalf(`IsCommented=true no Comment: wants '%q' got '%q'`, w, l) } // check without IsCommented hfl.IsCommented = false hfl.Comment = "" l = lineFormatter(hfl) w = "1.1.1.1 my.host.name" if !strings.EqualFold(l, w) { t.Fatalf(`IsCommented=false no Comment: wants '%q' got '%q'`, w, l) } // define a comment line hfl = HostsFileLine{ Number: 0, Type: 20, Address: []byte{}, Parts: []string{}, Hostnames: []string{}, Raw: "# Comment Line", Comment: "", IsCommented: false, trimed: "", } w = "# Comment line" l = lineFormatter(hfl) if !strings.EqualFold(l, w) { t.Fatalf(`Comment: wants '%q' got '%q'`, w, l) } } libhosty-1.1.0/go.mod000066400000000000000000000000571411167073700144650ustar00rootroot00000000000000module github.com/areYouLazy/libhosty go 1.16 libhosty-1.1.0/go.sum000066400000000000000000000000001411167073700144760ustar00rootroot00000000000000libhosty-1.1.0/helper.go000066400000000000000000000015511411167073700151650ustar00rootroot00000000000000package libhosty //RestoreDefaultWindowsHostsFile loads the default windows hosts file func (h *HostsFile) RestoreDefaultWindowsHostsFile() { hfl, _ := ParseHostsFileAsString(windowsHostsTemplate) h.HostsFileLines = hfl } //RestoreDefaultLinuxHostsFile loads the default linux hosts file func (h *HostsFile) RestoreDefaultLinuxHostsFile() { hfl, _ := ParseHostsFileAsString(linuxHostsTemplate) h.HostsFileLines = hfl } //RestoreDefaultDarwinHostsFile loads the default darwin hosts file func (h *HostsFile) RestoreDefaultDarwinHostsFile() { hfl, _ := ParseHostsFileAsString(darwinHostsTemplate) h.HostsFileLines = hfl } //AddDockerDesktopTemplate adds the dockerDesktopTemplate to the actual hostsFile func (h *HostsFile) AddDockerDesktopTemplate() { hfl, _ := ParseHostsFileAsString(dockerDesktopTemplate) h.HostsFileLines = append(h.HostsFileLines, hfl...) } libhosty-1.1.0/libhosty.go000066400000000000000000000521041411167073700155430ustar00rootroot00000000000000//Package libhosty is a pure golang library to manipulate the hosts file package libhosty import ( "io/ioutil" "net" "os" "regexp" "runtime" "strings" "sync" ) const ( //Version exposes library version Version = "2.0" ) const ( // defines default path for windows os windowsFilePath = "C:\\Windows\\System32\\drivers\\etc\\" // defines default path for linux os unixFilePath = "/etc/" // defines default filename hostsFileName = "hosts" ) //LineType define a safe type for line type enumeration type LineType int const ( //LineTypeUnknown defines unknown lines LineTypeUnknown LineType = 0 //LineTypeEmpty defines empty lines LineTypeEmpty LineType = 10 //LineTypeComment defines comment lines (starts with #) LineTypeComment LineType = 20 //LineTypeAddress defines address lines (actual hosts lines) LineTypeAddress LineType = 30 ) //HostsFileConfig defines parameters to find hosts file. // FilePath is the absolute path of the hosts file (filename included) type HostsFileConfig struct { FilePath string } //HostsFileLine holds hosts file lines data type HostsFileLine struct { //Number is the original line number Number int //LineType defines the line type Type LineType //Address is a net.IP representation of the address Address net.IP //Parts is a slice of the line splitted by '#' Parts []string //Hostnames is a slice of hostnames for the relative IP Hostnames []string //Raw is the raw representation of the line, as it is in the hosts file Raw string //Comment is the comment part of the line (if present in an ADDRESS line) Comment string //IsCommented to know if the current ADDRESS line is commented out (starts with '#') IsCommented bool //trimed is a trimed version (no spaces before and after) of the line trimed string } //HostsFile is a reference for the hosts file configuration and lines type HostsFile struct { sync.Mutex //Config reference to a HostsConfig object Config *HostsFileConfig //HostsFileLines slice of HostsFileLine objects HostsFileLines []HostsFileLine } //InitWithConfig returns a new instance of a hostsfile. // InitWithConfig is meant to be used with a custom conf file // however InitWithConfig() will fallback to Init() if conf is nill // You should use Init() to load hosts file from default location func InitWithConfig(conf *HostsFileConfig) (*HostsFile, error) { var config *HostsFileConfig var err error if conf != nil { config = conf } else { return Init() } // allocate a new HostsFile object hf := &HostsFile{ // use default configuration Config: config, // allocate a new slice of HostsFileLine objects HostsFileLines: make([]HostsFileLine, 0), } // parse the hosts file and load file lines hf.HostsFileLines, err = ParseHostsFile(hf.Config.FilePath) if err != nil { return nil, err } //return HostsFile return hf, nil } //Init returns a new instance of a hostsfile. func Init() (*HostsFile, error) { // initialize hostsConfig config, err := NewHostsFileConfig("") if err != nil { return nil, err } // allocate a new HostsFile object hf := &HostsFile{ // use default configuration Config: config, // allocate a new slice of HostsFileLine objects HostsFileLines: make([]HostsFileLine, 0), } // parse the hosts file and load file lines hf.HostsFileLines, err = ParseHostsFile(hf.Config.FilePath) if err != nil { return nil, err } //return HostsFile return hf, nil } //NewHostsFileConfig loads hosts file based on environment. // NewHostsFileConfig initialize the default file path based // on the OS or from a given location if a custom path is provided func NewHostsFileConfig(path string) (*HostsFileConfig, error) { // allocate hostsConfig var hc *HostsFileConfig // ensure custom path exists // https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go if fh, err := os.Stat(path); err == nil { // eusure custom path points to a file (not a directory) if !fh.IsDir() { hc = &HostsFileConfig{ FilePath: path, } } } else { // check os to construct default path switch runtime.GOOS { case "windows": hc = &HostsFileConfig{ FilePath: windowsFilePath + hostsFileName, } default: hc = &HostsFileConfig{ FilePath: unixFilePath + hostsFileName, } } } return hc, nil } //GetHostsFileLines returns every address row func (h *HostsFile) GetHostsFileLines() []*HostsFileLine { var hfl []*HostsFileLine for idx := range h.HostsFileLines { if h.HostsFileLines[idx].Type == LineTypeAddress { hfl = append(hfl, h.GetHostsFileLineByRow(idx)) } } return hfl } //GetHostsFileLineByRow returns a ponter to the given HostsFileLine row func (h *HostsFile) GetHostsFileLineByRow(row int) *HostsFileLine { return &h.HostsFileLines[row] } //GetHostsFileLineByIP returns the index of the line and a ponter to the given HostsFileLine line func (h *HostsFile) GetHostsFileLineByIP(ip net.IP) (int, *HostsFileLine) { if ip == nil { return -1, nil } for idx := range h.HostsFileLines { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { return idx, &h.HostsFileLines[idx] } } return -1, nil } func (h *HostsFile) GetHostsFileLinesByIP(ip net.IP) []*HostsFileLine { if ip == nil { return nil } hfl := make([]*HostsFileLine, 0) for idx := range h.HostsFileLines { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { hfl = append(hfl, &h.HostsFileLines[idx]) } } return hfl } //GetHostsFileLineByAddress returns the index of the line and a ponter to the given HostsFileLine line func (h *HostsFile) GetHostsFileLineByAddress(address string) (int, *HostsFileLine) { ip := net.ParseIP(address) return h.GetHostsFileLineByIP(ip) } func (h *HostsFile) GetHostsFileLinesByAddress(address string) []*HostsFileLine { ip := net.ParseIP(address) return h.GetHostsFileLinesByIP(ip) } //GetHostsFileLineByHostname returns the index of the line and a ponter to the given HostsFileLine line func (h *HostsFile) GetHostsFileLineByHostname(hostname string) (int, *HostsFileLine) { for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { return idx, &h.HostsFileLines[idx] } } } return -1, nil } func (h *HostsFile) GetHostsFileLinesByHostname(hostname string) []*HostsFileLine { hfl := make([]*HostsFileLine, 0) for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { hfl = append(hfl, &h.HostsFileLines[idx]) continue } } } return hfl } func (h *HostsFile) GetHostsFileLinesByHostnameAsRegexp(hostname string) []*HostsFileLine { hfl := make([]*HostsFileLine, 0) reg := regexp.MustCompile(hostname) for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if reg.MatchString(hn) { hfl = append(hfl, &h.HostsFileLines[idx]) continue } } } return hfl } //RenderHostsFile render and returns the hosts file with the lineFormatter() routine func (h *HostsFile) RenderHostsFile() string { // allocate a buffer for file lines var sliceBuffer []string // iterate HostsFileLines and popolate the buffer with formatted lines for _, l := range h.HostsFileLines { sliceBuffer = append(sliceBuffer, lineFormatter(l)) } // strings.Join() prevent the last line from being a new blank line // as opposite to a for loop with fmt.Printf(buffer + '\n') return strings.Join(sliceBuffer, "\n") } //RenderHostsFileLine render and returns the given hosts line with the lineFormatter() routine func (h *HostsFile) RenderHostsFileLine(row int) string { // iterate to find the row to render if len(h.HostsFileLines) > row { return lineFormatter(h.HostsFileLines[row]) } return "" } //SaveHostsFile write hosts file to configured path. // error is not nil if something goes wrong func (h *HostsFile) SaveHostsFile() error { return h.SaveHostsFileAs(h.Config.FilePath) } //SaveHostsFileAs write hosts file to the given path. // error is not nil if something goes wrong func (h *HostsFile) SaveHostsFileAs(path string) error { // render the file as a byte slice dataBytes := []byte(h.RenderHostsFile()) // write file to disk err := ioutil.WriteFile(path, dataBytes, 0644) if err != nil { return err } return nil } //RemoveHostsFileLineByRow remove row at given index from HostsFileLines func (h *HostsFile) RemoveHostsFileLineByRow(row int) { // prevent out-of-index if row < len(h.HostsFileLines) { h.Lock() h.HostsFileLines = append(h.HostsFileLines[:row], h.HostsFileLines[row+1:]...) h.Unlock() } } func (h *HostsFile) RemoveHostsFileLineByIP(ip net.IP) { for idx := len(h.HostsFileLines) - 1; idx >= 0; idx-- { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { h.RemoveHostsFileLineByRow(idx) return } } } func (h *HostsFile) RemoveHostsFileLinesByIP(ip net.IP) { for idx := len(h.HostsFileLines) - 1; idx >= 0; idx-- { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { h.RemoveHostsFileLineByRow(idx) } } } func (h *HostsFile) RemoveHostsFileLineByAddress(address string) { ip := net.ParseIP(address) h.RemoveHostsFileLineByIP(ip) } func (h *HostsFile) RemoveHostsFileLinesByAddress(address string) { ip := net.ParseIP(address) h.RemoveHostsFileLinesByIP(ip) } func (h *HostsFile) RemoveHostsFileLineByHostname(hostname string) { for idx := len(h.HostsFileLines) - 1; idx >= 0; idx-- { if h.HostsFileLines[idx].Type == LineTypeAddress { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { h.RemoveHostsFileLineByRow(idx) return } } } } } func (h *HostsFile) RemoveHostsFileLinesByHostnameAsRegexp(hostname string) { reg := regexp.MustCompile(hostname) for idx := len(h.HostsFileLines) - 1; idx >= 0; idx-- { for _, hn := range h.HostsFileLines[idx].Hostnames { if reg.MatchString(hn) { h.RemoveHostsFileLineByRow(idx) continue } } } } func (h *HostsFile) RemoveHostsFileLinesByHostname(hostname string) { for idx := len(h.HostsFileLines) - 1; idx >= 0; idx-- { if h.HostsFileLines[idx].Type == LineTypeAddress { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { h.RemoveHostsFileLineByRow(idx) continue } } } } } //LookupByHostname check if the given fqdn exists. // if yes, it returns the index of the address and the associated address. // error is not nil if something goes wrong func (h *HostsFile) LookupByHostname(hostname string) (int, net.IP, error) { for idx, hfl := range h.HostsFileLines { for _, hn := range hfl.Hostnames { if hn == hostname { return idx, h.HostsFileLines[idx].Address, nil } } } return -1, nil, ErrHostnameNotFound } //AddHostsFileLineRaw add the given ip/fqdn/comment pair // this is different from AddHostFileLine because it does not take care of duplicates // this just append the new entry to the hosts file func (h *HostsFile) AddHostsFileLineRaw(ipRaw, fqdnRaw, comment string) (int, *HostsFileLine, error) { // hostname to lowercase hostname := strings.ToLower(fqdnRaw) // parse ip to net.IP ip := net.ParseIP(ipRaw) // if we have a valid IP if ip != nil { // create a new hosts line hfl := HostsFileLine{ Type: LineTypeAddress, Address: ip, Hostnames: []string{hostname}, Comment: comment, IsCommented: false, } // append to hosts h.HostsFileLines = append(h.HostsFileLines, hfl) // get index idx := len(h.HostsFileLines) - 1 // return created entry return idx, &h.HostsFileLines[idx], nil } // return error return -1, nil, ErrCannotParseIPAddress(ipRaw) } //AddHostsFileLine add the given ip/fqdn/comment pair, cleanup is done for previous entry. // it returns the index of the edited (created) line and a pointer to the hostsfileline object. // error is not nil if something goes wrong func (h *HostsFile) AddHostsFileLine(ipRaw, fqdnRaw, comment string) (int, *HostsFileLine, error) { // hostname to lowercase hostname := strings.ToLower(fqdnRaw) // parse ip to net.IP ip := net.ParseIP(ipRaw) // if we have a valid IP if ip != nil { //check if we alredy have the fqdn if idx, addr, err := h.LookupByHostname(hostname); err == nil { //if actual ip is the same as the given one, we are done if net.IP.Equal(addr, ip) { // handle comment if comment != "" { // just replace the current comment with the new one h.HostsFileLines[idx].Comment = comment } return idx, &h.HostsFileLines[idx], nil } //if address is different, we need to remove the hostname from the previous entry for hostIdx, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { if len(h.HostsFileLines[idx].Hostnames) > 1 { h.Lock() h.HostsFileLines[idx].Hostnames = append(h.HostsFileLines[idx].Hostnames[:hostIdx], h.HostsFileLines[idx].Hostnames[hostIdx+1:]...) h.Unlock() } //remove the line if there are no more hostnames (other than the actual one) if len(h.HostsFileLines[idx].Hostnames) <= 1 { h.RemoveHostsFileLineByRow(idx) } } } } //if we alredy have the address, just add the hostname to that line for idx, hfl := range h.HostsFileLines { if net.IP.Equal(hfl.Address, ip) { h.Lock() h.HostsFileLines[idx].Hostnames = append(h.HostsFileLines[idx].Hostnames, hostname) h.Unlock() // handle comment if comment != "" { // just replace the current comment with the new one h.HostsFileLines[idx].Comment = comment } // return edited entry return idx, &h.HostsFileLines[idx], nil } } // at this point we need to create new host line hfl := HostsFileLine{ Type: LineTypeAddress, Address: ip, Hostnames: []string{hostname}, Comment: comment, IsCommented: false, } // generate raw version of the line hfl.Raw = lineFormatter(hfl) // append to hosts h.HostsFileLines = append(h.HostsFileLines, hfl) // get index idx := len(h.HostsFileLines) - 1 // return created entry return idx, &h.HostsFileLines[idx], nil } // return error return -1, nil, ErrCannotParseIPAddress(ipRaw) } //AddCommentFileLine adds a new line of type comment with the given comment. // it returns the index of the edited (created) line and a pointer to the hostsfileline object. // error is not nil if something goes wrong func (h *HostsFile) AddCommentFileLine(comment string) (int, *HostsFileLine, error) { h.Lock() defer h.Unlock() hfl := HostsFileLine{ Type: LineTypeComment, Raw: "# " + comment, Comment: comment, } hfl.Raw = lineFormatter(hfl) h.HostsFileLines = append(h.HostsFileLines, hfl) idx := len(h.HostsFileLines) - 1 return idx, &h.HostsFileLines[idx], nil } //AddEmptyFileLine adds a new line of type empty. // it returns the index of the edited (created) line and a pointer to the hostsfileline object. // error is not nil if something goes wrong func (h *HostsFile) AddEmptyFileLine() (int, *HostsFileLine, error) { h.Lock() defer h.Unlock() hfl := HostsFileLine{ Type: LineTypeEmpty, Raw: "", } h.HostsFileLines = append(h.HostsFileLines, hfl) idx := len(h.HostsFileLines) - 1 return idx, &h.HostsFileLines[idx], nil } //CommentHostsFileLineByRow set the IsCommented bit for the given row to true func (h *HostsFile) CommentHostsFileLineByRow(row int) error { h.Lock() defer h.Unlock() if len(h.HostsFileLines) > row { if h.HostsFileLines[row].Type == LineTypeAddress { if !h.HostsFileLines[row].IsCommented { h.HostsFileLines[row].IsCommented = true h.HostsFileLines[row].Raw = h.RenderHostsFileLine(row) return nil } return ErrAlredyCommentedLine } return ErrNotAnAddressLine } return ErrUnknown } //CommentHostsFileLineByIP set the IsCommented bit for the given address to true func (h *HostsFile) CommentHostsFileLineByIP(ip net.IP) error { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { if !h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = true h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) return nil } return ErrAlredyCommentedLine } } return ErrAddressNotFound } func (h *HostsFile) CommentHostsFileLinesByIP(ip net.IP) { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { if !h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = true h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) } } } } //CommentHostsFileLineByAddress set the IsCommented bit for the given address as string to true func (h *HostsFile) CommentHostsFileLineByAddress(address string) error { ip := net.ParseIP(address) return h.CommentHostsFileLineByIP(ip) } func (h *HostsFile) CommentHostsFileLinesByAddress(address string) { ip := net.ParseIP(address) h.CommentHostsFileLinesByIP(ip) } //CommentHostsFileLineByHostname set the IsCommented bit for the given hostname to true func (h *HostsFile) CommentHostsFileLineByHostname(hostname string) error { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { if !h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = true h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) return nil } return ErrAlredyCommentedLine } } } return ErrHostnameNotFound } func (h *HostsFile) CommentHostsFileLinesByHostname(hostname string) { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { if !h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = true h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) } } } } } func (h *HostsFile) CommentHostsFileLinesByHostnameAsRegexp(hostname string) { h.Lock() defer h.Unlock() reg := regexp.MustCompile(hostname) for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if reg.MatchString(hn) { if !h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = true h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) continue } } } } } //UncommentHostsFileLineByRow set the IsCommented bit for the given row to false func (h *HostsFile) UncommentHostsFileLineByRow(row int) error { h.Lock() defer h.Unlock() if len(h.HostsFileLines) > row { if h.HostsFileLines[row].Type == LineTypeAddress { if h.HostsFileLines[row].IsCommented { h.HostsFileLines[row].IsCommented = false h.HostsFileLines[row].Raw = h.RenderHostsFileLine(row) return nil } return ErrAlredyUncommentedLine } return ErrNotAnAddressLine } return ErrUnknown } //UncommentHostsFileLineByIP set the IsCommented bit for the given address to false func (h *HostsFile) UncommentHostsFileLineByIP(ip net.IP) error { h.Lock() defer h.Unlock() for idx, hfl := range h.HostsFileLines { if net.IP.Equal(ip, hfl.Address) { if h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = false h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) return nil } return ErrAlredyUncommentedLine } } return ErrNotAnAddressLine } func (h *HostsFile) UncommentHostsFileLinesByIP(ip net.IP) { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { if net.IP.Equal(ip, h.HostsFileLines[idx].Address) { if h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = false h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) } } } } //UncommentHostsFileLineByAddress set the IsCommented bit for the given address as string to false func (h *HostsFile) UncommentHostsFileLineByAddress(address string) error { ip := net.ParseIP(address) return h.UncommentHostsFileLineByIP(ip) } func (h *HostsFile) UncommentHostsFileLinesByAddress(address string) { ip := net.ParseIP(address) h.UncommentHostsFileLinesByIP(ip) } //UncommentHostsFileLineByHostname set the IsCommented bit for the given hostname to false func (h *HostsFile) UncommentHostsFileLineByHostname(hostname string) error { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { if h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = false h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) return nil } return ErrAlredyUncommentedLine } } } return ErrHostnameNotFound } func (h *HostsFile) UncommentHostsFileLinesByHostname(hostname string) { h.Lock() defer h.Unlock() for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if hn == hostname { if h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = false h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) } } } } } func (h *HostsFile) UncommentHostsFileLinesByHostnameAsRegexp(hostname string) { h.Lock() defer h.Unlock() reg := regexp.MustCompile(hostname) for idx := range h.HostsFileLines { for _, hn := range h.HostsFileLines[idx].Hostnames { if reg.MatchString(hn) { if h.HostsFileLines[idx].IsCommented { h.HostsFileLines[idx].IsCommented = false h.HostsFileLines[idx].Raw = h.RenderHostsFileLine(idx) continue } } } } } libhosty-1.1.0/libhosty_test.go000066400000000000000000000213441411167073700166040ustar00rootroot00000000000000package libhosty import ( "net" "runtime" "strings" "testing" ) var hf *HostsFile var hc *HostsFileConfig func TestInit(t *testing.T) { var err error hf, err = Init() if err != nil { t.Fatal(err) } if hf == nil { t.Fatal("hostsFile is nil") } if len(hf.HostsFileLines) <= 0 { t.Fatalf("we should have at least 1 line") } } func TestNewHostsFileConfig(t *testing.T) { var err error hc, err = NewHostsFileConfig("") if err != nil { t.Fatal(err) } switch runtime.GOOS { case "windows": if res := strings.Compare(hc.FilePath, "C:\\Windows\\System32\\drivers\\etc\\hosts"); res != 0 { t.Fatalf("error in hostsConfig path: wants %q got %q", "C:\\Windows\\System32\\drivers\\etc\\hosts", hc.FilePath) } default: if res := strings.Compare(hc.FilePath, "/etc/hosts"); res != 0 { t.Fatalf("error in hostsConfig path: wants %q got %q", "/etc/hosts", hc.FilePath) } } hc, err := NewHostsFileConfig("/etc") if err != nil { t.Fatal(err) } if res := strings.Compare(hf.Config.FilePath, "/etc/hosts"); res != 0 { t.Fatalf("should have %q got %q", "/etc/hosts", hc.FilePath) } hc, err = NewHostsFileConfig("/etc/hosts") if err != nil { t.Fatal(err) } if res := strings.Compare(hc.FilePath, "/etc/hosts"); res != 0 { t.Fatalf("error in hostsConfig path: wants %q got %q", "/etc/hosts", hc.FilePath) } } func TestInitWithConf(t *testing.T) { var err error //test with conf = nil hf, err = InitWithConfig(nil) if err != nil { t.Fatal(err) } // test with conf hf, err = InitWithConfig(hc) if err != nil { t.Fatal(err) } if res := strings.Compare(hf.Config.FilePath, "/etc/hosts"); res != 0 { t.Fatalf("error in InitWithConfig path: wants %q got %q", "/etc/hosts", hc.FilePath) } } func TestGetHostsFileLineByRow(t *testing.T) { idx, _, _ := hf.AddHostsFileLine("9.9.9.9", "gethostsfilelinebyrow.libhosty.local", "") hfl := hf.GetHostsFileLineByRow(idx) if hfl.Number != 0 { t.Fatalf("error: wants %d got %d", idx, hfl.Number) } } func TestGetHostsFileLineByIP(t *testing.T) { _, _, err := hf.AddHostsFileLine("8.8.8.8", "gethostsfilelinebyip.libhosty.local", "") if err != nil { t.Fatal(err) } ip := net.ParseIP("8.8.8.8") _, hfl := hf.GetHostsFileLineByIP(ip) if !net.IP.Equal(ip, hfl.Address) { t.Fatalf("error: wants %q got %q", ip, hfl.Address) } ip = net.ParseIP("fa.ke.i.p") idx, _ := hf.GetHostsFileLineByIP(ip) if idx != -1 { t.Fatalf("wants %d got %d", -1, idx) } } func TestGetHostsFileLineByAddress(t *testing.T) { hf.AddHostsFileLine("7.7.7.7", "gethostsfilelinebyaddress.libhosty.local", "") _, hfl := hf.GetHostsFileLineByAddress("7.7.7.7") if res := strings.Compare(hfl.Address.String(), "7.7.7.7"); res != 0 { t.Fatalf("error: wants %q got %q", "7.7.7.7", hfl.Address.String()) } } func TestGetHostsFileLineByHostname(t *testing.T) { hf.AddHostsFileLine("6.6.6.6", "gethostsfilelinebyhostname.libhosty.local", "") _, hfl := hf.GetHostsFileLineByHostname("gethostsfilelinebyhostname.libhosty.local") res := false for _, v := range hfl.Hostnames { if v == "gethostsfilelinebyhostname.libhosty.local" { res = true } } if res != true { t.Fatalf("error: missing localhost in hostnames: %s", hfl.Hostnames) } idx, _ := hf.GetHostsFileLineByHostname("") if idx != -1 { t.Fatalf("wants %d got %d", -1, idx) } } func TestAddCommentFileLine(t *testing.T) { hf.AddCommentFileLine("comment") hfl := hf.HostsFileLines[len(hf.HostsFileLines)-1] if hfl.Type != LineTypeComment { t.Fatalf("expecting comment line, found %q", hfl.Type) } if res := strings.Compare(hfl.Raw, "# comment"); res != 0 { t.Fatalf("wants %q got %q", "# comment", hfl.Raw) } } func TestAddEmptyFileLine(t *testing.T) { idx, _, err := hf.AddEmptyFileLine() if err != nil { t.Fatal(err) } hfl := hf.HostsFileLines[idx] if hfl.Type != LineTypeEmpty { t.Fatalf("expecting empty line, found %q", hfl.Type) } } func TestAddHostsFileLine(t *testing.T) { idx, _, err := hf.AddHostsFileLine("5.5.5.5", "addhost.libhosty.local", "my comment") if err != nil { t.Fatal(err) } hfl := hf.HostsFileLines[idx] if hfl.Type != LineTypeAddress { t.Fatalf("expecting address line, found %q", hfl.Type) } if res := strings.Compare(hfl.Address.String(), "5.5.5.5"); res != 0 { t.Fatalf("expecting %q, found %q", "5.5.5.5", hfl.Address.String()) } r := false for _, v := range hfl.Hostnames { if res := strings.Compare(v, "addhost.libhosty.local"); res == 0 { r = true } } if !r { t.Fatalf("mssing %q in hostnames: got %q", "addhost.libhosty.local", hfl.Hostnames) } if res := strings.Compare(hfl.Comment, "my comment"); res != 0 { t.Fatalf("expecting %q, found %q", "my comment", hfl.Comment) } _, _, err = hf.AddHostsFileLine("5.5.5.5", "addhost.libhosty.local", "") if err != nil { t.Fatal(err) } _, _, err = hf.AddHostsFileLine("5.5.5.5", "addhost2.libhosty.local", "") if err != nil { t.Fatal(err) } _, _, err = hf.AddHostsFileLine("5.5.5.6", "addhost2.libhosty.local", "") if err != nil { t.Fatal(err) } } func TestAddHostsFileLineRaw(t *testing.T) { idx, _, err := hf.AddHostsFileLineRaw("4.4.4.4", "addhostraw.libhosty.local", "my comment") if err != nil { t.Fatal(err) } hfl := hf.HostsFileLines[idx] if hfl.Type != LineTypeAddress { t.Fatalf("expecting address line, found %q", hfl.Type) } if res := strings.Compare(hfl.Address.String(), "4.4.4.4"); res != 0 { t.Fatalf("expecting %q, found %q", "4.4.4.4", hfl.Address.String()) } r := false for _, v := range hfl.Hostnames { if res := strings.Compare(v, "addhostraw.libhosty.local"); res == 0 { r = true } } if !r { t.Fatalf("mssing %q in hostnames: got %q", "addhostraw.libhosty.local", hfl.Hostnames) } if res := strings.Compare(hfl.Comment, "my comment"); res != 0 { t.Fatalf("expecting %q, found %q", "my comment", hfl.Comment) } idx, _, _ = hf.AddHostsFileLineRaw("fa.ke.i.p", "addhostraw.libhosty.local", "") if idx != -1 { t.Fatalf("wants %d got %d", -1, idx) } } func TestCommentHostsFileLineByRow(t *testing.T) { idx, hfl, err := hf.AddHostsFileLine("3.3.3.3", "commentbyrow.host.name", "") if err != nil { t.Fatal(err) } if hfl.IsCommented { t.Fatal("new line is commented") } err = hf.CommentHostsFileLineByRow(idx) if err != nil { t.Fatal(err) } hfl2 := hf.GetHostsFileLineByRow(idx) if err != nil { t.Fatal(err) } if !hfl2.IsCommented { t.Fatal("line should be commented") } } func TestUncommentHostsFileLineByRow(t *testing.T) { idx, _ := hf.GetHostsFileLineByAddress("3.3.3.3") err := hf.UncommentHostsFileLineByRow(idx) if err != nil { t.Fatal(err) } hfl := hf.GetHostsFileLineByRow(idx) if hfl.IsCommented { t.Fatal("line should be uncommented") } } func TestCommentHostsFileLineByIP(t *testing.T) { _, _, err := hf.AddHostsFileLine("2.2.2.2", "commentbyip.libhosty.local", "") if err != nil { t.Fatal(err) } ip := net.ParseIP("2.2.2.2") err = hf.CommentHostsFileLineByIP(ip) if err != nil { t.Fatal(err) } _, hfl2 := hf.GetHostsFileLineByIP(ip) if err != nil { t.Fatal(err) } if !hfl2.IsCommented { t.Fatal("line should be commented") } } func TestUncommentHostsFileLineByIP(t *testing.T) { ip := net.ParseIP("2.2.2.2") err := hf.UncommentHostsFileLineByIP(ip) if err != nil { t.Fatal(err) } _, hfl := hf.GetHostsFileLineByIP(ip) if hfl.IsCommented { t.Fatal("line should be uncommented") } } func TestCommentHostsFileLineByAddress(t *testing.T) { _, _, err := hf.AddHostsFileLine("2.2.2.3", "commentbyaddress.libhosty.local", "") if err != nil { t.Fatal(err) } err = hf.CommentHostsFileLineByAddress("2.2.2.3") if err != nil { t.Fatal(err) } _, hfl2 := hf.GetHostsFileLineByAddress("2.2.2.3") if err != nil { t.Fatal(err) } if !hfl2.IsCommented { t.Fatal("line should be commented") } } func TestUncommentHostsFileLineByAddress(t *testing.T) { err := hf.UncommentHostsFileLineByAddress("2.2.2.3") if err != nil { t.Fatal(err) } _, hfl := hf.GetHostsFileLineByAddress("2.2.2.3") if hfl.IsCommented { t.Fatal("line should be uncommented") } } func TestCommentHostsFileLineByHostname(t *testing.T) { _, _, err := hf.AddHostsFileLine("2.2.2.4", "commentbyhostname.libhosty.local", "") if err != nil { t.Fatal(err) } err = hf.CommentHostsFileLineByHostname("commentbyhostname.libhosty.local") if err != nil { t.Fatal(err) } _, hfl2 := hf.GetHostsFileLineByHostname("commentbyhostname.libhosty.local") if err != nil { t.Fatal(err) } if !hfl2.IsCommented { t.Fatal("line should be commented") } } func TestUncommentHostsFileLineByHostname(t *testing.T) { err := hf.UncommentHostsFileLineByHostname("commentbyhostname.libhosty.local") if err != nil { t.Fatal(err) } _, hfl := hf.GetHostsFileLineByHostname("commentbyhostname.libhosty.local") if hfl.IsCommented { t.Fatal("line should be uncommented") } } libhosty-1.1.0/parser.go000066400000000000000000000055641411167073700152120ustar00rootroot00000000000000package libhosty import ( "io/ioutil" "net" "strings" ) //ParseHostsFile parse a hosts file from the given location. // error is not nil if something goes wrong func ParseHostsFile(path string) ([]HostsFileLine, error) { byteData, err := ioutil.ReadFile(path) if err != nil { return nil, err } return parser(byteData) } //ParseHostsFileAsString parse a hosts file from a given string. // error is not nil if something goes wrong func ParseHostsFileAsString(stringData string) ([]HostsFileLine, error) { bytesData := []byte(stringData) return parser(bytesData) } func parser(bytesData []byte) ([]HostsFileLine, error) { byteDataNormalized := strings.Replace(string(bytesData), "\r\n", "\n", -1) fileLines := strings.Split(byteDataNormalized, "\n") hostsFileLines := make([]HostsFileLine, len(fileLines)) // trim leading an trailing whitespace for i, l := range fileLines { curLine := &hostsFileLines[i] curLine.Number = i curLine.Raw = l // trim line curLine.trimed = strings.TrimSpace(l) // check if it's an empty line if curLine.trimed == "" { curLine.Type = LineTypeEmpty continue } // check if line starts with a # if strings.HasPrefix(curLine.trimed, "#") { // this can be a comment or a commented host line // so remove the 1st char (#), trim spaces // and try to parse the line as a host line noCommentLine := strings.TrimPrefix(curLine.trimed, "#") tmpParts := strings.Fields(strings.TrimSpace(noCommentLine)) // check what we have switch len(tmpParts) { case 0: // empty line, comment line curLine.Type = LineTypeComment continue default: // non-empty line, try to parse as address address := net.ParseIP(tmpParts[0]) // if address is nil this line is a comment if address == nil { curLine.Type = LineTypeComment continue } } // otherwise it is a commented line so let's try to parse it as a normal line curLine.IsCommented = true curLine.trimed = noCommentLine } // not a comment or empty line so try to parse it // check if it contains a comment curLineSplit := strings.SplitN(curLine.trimed, "#", 2) if len(curLineSplit) > 1 { // trim spaces from comments curLine.Comment = strings.TrimSpace(curLineSplit[1]) } curLine.trimed = curLineSplit[0] curLine.Parts = strings.Fields(curLine.trimed) if len(curLine.Parts) > 1 { // parse address to ensure we have a valid address line tmpIP := net.ParseIP(curLine.Parts[0]) if tmpIP != nil { curLine.Type = LineTypeAddress curLine.Address = tmpIP // lower case all for _, p := range curLine.Parts[1:] { curLine.Hostnames = append(curLine.Hostnames, strings.ToLower(p)) } continue } } // if we can't figure out what this line is mark it as unknown curLine.Type = LineTypeUnknown } // normalize slice hostsFileLines = hostsFileLines[:] return hostsFileLines, nil } libhosty-1.1.0/parser_test.go000066400000000000000000000046301411167073700162420ustar00rootroot00000000000000package libhosty import ( "runtime" "strings" "testing" ) //TestParseHostsFile implicitly tests parser() // assuming the test is executed on a computer, // we should be able to load the default hosts file func TestParseHostsFile(t *testing.T) { var path string // check for invalid path path = "/my/custom/invalid/path/hosts" _, err := ParseHostsFile(path) if err == nil { t.Fatalf("should fail with invalid path: %s", path) } // define file path switch runtime.GOOS { case "win": path = "C:\\Windows\\System32\\drivers\\etc\\hosts" default: path = "/etc/hosts" } // parse file hf, err := ParseHostsFile(path) // check for errors if err != nil { t.Fatalf("error parsing hosts file: %s", err) } // check for unknown lines // this can be a false positive if the line is actually invalid // also we assume to have no unknown line in a standard system // but we left it to ensure correct lines are not recognized as unknown lines // // edit: this now breaks the github automatic build due to https://github.com/actions/virtual-environments/issues/3353 // so we must comment this out // // for k, v := range hf { // if v.Type == LineTypeUnknown { // t.Fatalf("unable to parse line at index %d: %s", k, v.Raw) // } // } // for every address line, ensure we have a valid ip address for k, v := range hf { if v.Type == LineTypeAddress { if v.Address == nil { t.Fatalf("address line without address at index %d: %v", k, v) } } } // for every comment line ensure it starts with # for k, v := range hf { if v.Type == LineTypeComment { if !strings.HasPrefix(v.Raw, "#") { t.Fatalf("comment line does not starts with # at index %d: %v", k, v) } } } } //TestParseHostsFileAsString test unknown line type // actual parsing is already tested in TestParseHostsFile() func TestParseHostsFileAsString(t *testing.T) { // define a custom hosts file with an address line and an invalid line" var fakeHostsFile = `1.1.1.1 my.hosts.file # With Comment 129dj120isdj12i0 1092jd 210dk` hf, err := ParseHostsFileAsString(fakeHostsFile) if err != nil { t.Fatalf("error parsing fakeHostsFile: %s", err) } // we expect the 1st line to be of type address if hf[0].Type != LineTypeAddress { t.Fatalf("line should be of type address: %v", hf[0]) } // and the 2nd line to be of type unknown if hf[1].Type != LineTypeUnknown { t.Fatalf("line should be of type address: %v", hf[1]) } } libhosty-1.1.0/templates.go000066400000000000000000000034331411167073700157050ustar00rootroot00000000000000package libhosty // linuxHostsTemplate defines default linux hosts file const linuxHostsTemplate = `# Do not remove the following line, or various programs # that require network functionality will fail. 127.0.0.1 localhost.localdomain localhost ::1 localhost6.localdomain6 localhost6 ` // windowsHostsTemplate defines default windows hosts file const windowsHostsTemplate = `# Copyright (c) 1993-2006 Microsoft Corp. # # This is a sample HOSTS file used by Microsoft TCP/IP for Windows. # # This file contains the mappings of IP addresses to host names. Each # entry should be kept on an individual line. The IP address should # be placed in the first column followed by the corresponding host name. # The IP address and the host name should be separated by at least one # space. # # Additionally, comments (such as these) may be inserted on individual # lines or following the machine name denoted by a '#' symbol. # # For example: # # 102.54.94.97 rhino.acme.com # source server # 38.25.63.10 x.acme.com # x client host # localhost name resolution is handle within DNS itself. # 127.0.0.1 localhost # ::1 localhost ` // darwinHostsTemplate defines default darwin hosts file const darwinHostsTemplate = `## # Host Database # # localhost is used to configure the loopback interface # when the system is booting. Do not change this entry ## 127.0.0.1 localhost 255.255.255.255 broadcasthost::1 localhost ::1 localhost fe80::1%lo0 localhost ` // dockerDesktopTemplate defines docker desktop hosts entry const dockerDesktopTemplate = `# Added by Docker Desktop # To allow the same kube context to work on the host and the container: 127.0.0.1 kubernetes.docker.internal # End of section `