diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 1c8390a..b429938 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -27,4 +27,4 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5022986..029a751 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,11 +33,11 @@ jobs: go-version: ^1.18 - uses: actions/checkout@v3 - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v9 # See https://github.com/golangci/golangci-lint-action/issues/485 for why we do --out-.... with: version: latest - args: "-E stylecheck,revive,gocritic --out-${NO_FUTURE}format colored-line-number --timeout 5m" + args: "-E staticcheck,revive,gocritic --timeout 5m" build-cli: name: Build CLI @@ -69,4 +69,4 @@ jobs: - uses: actions/setup-go@v3 with: go-version: ^1.18 - - run: make test \ No newline at end of file + - run: make test diff --git a/cmd/geteduroam-cli/main.go b/cmd/geteduroam-cli/main.go index a0a6c29..fab2274 100644 --- a/cmd/geteduroam-cli/main.go +++ b/cmd/geteduroam-cli/main.go @@ -16,15 +16,15 @@ import ( "golang.org/x/sys/unix" "golang.org/x/term" + "github.com/geteduroam/linux-app/internal/clientver" "github.com/geteduroam/linux-app/internal/discovery" "github.com/geteduroam/linux-app/internal/handler" - "github.com/geteduroam/linux-app/internal/log" + "github.com/geteduroam/linux-app/internal/logwrap" "github.com/geteduroam/linux-app/internal/network" "github.com/geteduroam/linux-app/internal/notification" "github.com/geteduroam/linux-app/internal/provider" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "github.com/geteduroam/linux-app/internal/variant" - "github.com/geteduroam/linux-app/internal/version" ) // IsTerminal return true if the file descriptor is terminal. @@ -445,7 +445,7 @@ func main() { var local string var url string program := fmt.Sprintf("%s-cli", variant.DisplayName) - lpath, err := log.Location(program) + lpath, err := logwrap.Location(program) if err != nil { lpath = "N/A" } @@ -466,11 +466,11 @@ func main() { return } if verbose { - utils.IsVerbose = true + utilsx.IsVerbose = true } - log.Initialize(program, debug) + logwrap.Initialize(program, debug) if versionf { - fmt.Println(version.Get()) + fmt.Println(clientver.Get()) return } if !IsTerminal() { @@ -503,13 +503,13 @@ func main() { if vEnd == nil { return } - fmt.Printf("Your profile is valid for: %d days\n", utils.ValidityDays(*vEnd)) + fmt.Printf("Your profile is valid for: %d days\n", utilsx.ValidityDays(*vEnd)) curr := time.Now() if vBeg != nil && curr.Before(*vBeg) { delta := vBeg.Sub(curr) // if there is more than 5 second difference we show a countdown if delta > 5*time.Second { - fmt.Printf("And you can start using the profile in: %s\n", utils.DeltaTime(delta, "", "")) + fmt.Printf("And you can start using the profile in: %s\n", utilsx.DeltaTime(delta, "", "")) } } if !notification.HasDaemonSupport() { diff --git a/cmd/geteduroam-gui/main.go b/cmd/geteduroam-gui/main.go index a44c5ee..5468f3b 100644 --- a/cmd/geteduroam-gui/main.go +++ b/cmd/geteduroam-gui/main.go @@ -20,13 +20,13 @@ import ( "github.com/jwijenbergh/puregotk/v4/glib" "github.com/jwijenbergh/puregotk/v4/gtk" + "github.com/geteduroam/linux-app/internal/clientver" "github.com/geteduroam/linux-app/internal/discovery" "github.com/geteduroam/linux-app/internal/handler" - "github.com/geteduroam/linux-app/internal/log" + "github.com/geteduroam/linux-app/internal/logwrap" "github.com/geteduroam/linux-app/internal/network" "github.com/geteduroam/linux-app/internal/provider" "github.com/geteduroam/linux-app/internal/variant" - "github.com/geteduroam/linux-app/internal/version" ) type serverList struct { @@ -364,14 +364,14 @@ func (m *mainState) initBurger() { awin.SetLogo(texture) defer texture.Unref() } - lpath, err := log.Location(fmt.Sprintf("%s-gui", variant.DisplayName)) + lpath, err := logwrap.Location(fmt.Sprintf("%s-gui", variant.DisplayName)) if err == nil { awin.SetSystemInformation("Log location: " + lpath) } awin.SetProgramName(fmt.Sprintf("%s GUI", variant.DisplayName)) awin.SetComments(fmt.Sprintf("Client to easily and securely configure %s", variant.ProfileName)) awin.SetAuthors([]string{"Jeroen Wijenbergh", "Martin van Es", "Alexandru Cacean"}) - awin.SetVersion(version.Get()) + awin.SetVersion(clientver.Get()) awin.SetWebsite("https://github.com/geteduroam/linux-app") // SetLicenseType has a scary warning: "comes with absolutely no warranty" // While it is true according to the license, I find it unfriendly @@ -478,7 +478,7 @@ func main() { var debug bool var gtkarg string program := fmt.Sprintf("%s-gui", variant.DisplayName) - lpath, err := log.Location(program) + lpath, err := logwrap.Location(program) if err != nil { lpath = "N/A" } @@ -496,7 +496,7 @@ func main() { } if versionf { - fmt.Println(version.Get()) + fmt.Println(clientver.Get()) return } @@ -528,7 +528,7 @@ func main() { glib.LogSetDefaultHandler(&handler, 0) - log.Initialize(program, debug) + logwrap.Initialize(program, debug) ui := ui{} args := []string{os.Args[0]} if gtkarg != "" { diff --git a/cmd/geteduroam-gui/success.go b/cmd/geteduroam-gui/success.go index 3432972..33cfd3a 100644 --- a/cmd/geteduroam-gui/success.go +++ b/cmd/geteduroam-gui/success.go @@ -5,7 +5,7 @@ import ( "time" "github.com/geteduroam/linux-app/internal/notification" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "github.com/geteduroam/linux-app/internal/variant" "github.com/jwijenbergh/puregotk/v4/adw" "github.com/jwijenbergh/puregotk/v4/glib" @@ -74,12 +74,12 @@ func (s *SuccessState) Initialize() { delta := time.Until(*s.vBeg) // We do not want to show on 0 seconds if delta >= 1*time.Second { - valid.SetMarkup(fmt.Sprintf("Your profile will be valid in: %s", utils.DeltaTime(delta, "", ""))) + valid.SetMarkup(fmt.Sprintf("Your profile will be valid in: %s", utilsx.DeltaTime(delta, "", ""))) valid.Show() return true } if s.vEnd != nil { - valid.SetMarkup(fmt.Sprintf("%s %d days", validText, utils.ValidityDays(*s.vEnd))) + valid.SetMarkup(fmt.Sprintf("%s %d days", validText, utilsx.ValidityDays(*s.vEnd))) } else { // not very realistic this happens, but in theory it could valid.SetMarkup("Your profile is valid") } @@ -99,7 +99,7 @@ func (s *SuccessState) Initialize() { return } - dialog := gtk.NewMessageDialog(s.parent, gtk.DialogDestroyWithParentValue, gtk.MessageQuestionValue, gtk.ButtonsYesNoValue, "This connection profile will expire in %i days.\n\nDo you want to enable notifications that warn for imminent expiry using systemd?", utils.ValidityDays(*s.vEnd)) + dialog := gtk.NewMessageDialog(s.parent, gtk.DialogDestroyWithParentValue, gtk.MessageQuestionValue, gtk.ButtonsYesNoValue, "This connection profile will expire in %i days.\n\nDo you want to enable notifications that warn for imminent expiry using systemd?", utilsx.ValidityDays(*s.vEnd)) dialog.Present() var dialogcb func(gtk.Dialog, int) dialogcb = func(_ gtk.Dialog, response int) { diff --git a/cmd/geteduroam-gui/util.go b/cmd/geteduroam-gui/util.go index 16887d8..f3066dd 100644 --- a/cmd/geteduroam-gui/util.go +++ b/cmd/geteduroam-gui/util.go @@ -51,7 +51,7 @@ func bytesPixbuf(b []byte) (*gdkpixbuf.Pixbuf, error) { if err != nil { return nil, err } - defer os.Remove(f.Name()) + defer os.Remove(f.Name()) //nolint:errcheck _, err = f.Write(b) if err != nil { return nil, err diff --git a/cmd/geteduroam-notifcheck/main.go b/cmd/geteduroam-notifcheck/main.go index 73c8c27..a53b8fe 100644 --- a/cmd/geteduroam-notifcheck/main.go +++ b/cmd/geteduroam-notifcheck/main.go @@ -9,7 +9,7 @@ import ( "golang.org/x/exp/slog" "github.com/geteduroam/linux-app/internal/config" - "github.com/geteduroam/linux-app/internal/log" + "github.com/geteduroam/linux-app/internal/logwrap" "github.com/geteduroam/linux-app/internal/nm" "github.com/geteduroam/linux-app/internal/notification" "github.com/geteduroam/linux-app/internal/variant" @@ -43,13 +43,13 @@ func hasValidProfile(uuids []string) bool { func main() { program := fmt.Sprintf("%s-notifcheck", variant.DisplayName) - lpath, err := log.Location(program) + lpath, err := logwrap.Location(program) if err != nil { lpath = "N/A" } flag.Usage = func() { fmt.Printf(usage, program, variant.DisplayName, lpath) } flag.Parse() - log.Initialize(fmt.Sprintf("%s-notifcheck", variant.DisplayName), false) + logwrap.Initialize(fmt.Sprintf("%s-notifcheck", variant.DisplayName), false) cfg, err := config.Load() if err != nil { slog.Error("no previous state", "error", err) diff --git a/internal/version/version.go b/internal/clientver/clientver.go similarity index 89% rename from internal/version/version.go rename to internal/clientver/clientver.go index d526fed..bf3c3b5 100644 --- a/internal/version/version.go +++ b/internal/clientver/clientver.go @@ -1,4 +1,5 @@ -package version +// Package clientver implements version info for the client +package clientver import ( "fmt" diff --git a/internal/config/config.go b/internal/config/config.go index 21280ca..86229b9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -// package config has methods to write (TODO: read) config files +// Package config has methods to write (TODO: read) config files package config import ( @@ -9,7 +9,7 @@ import ( "golang.org/x/exp/slog" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "github.com/geteduroam/linux-app/internal/variant" ) @@ -50,7 +50,7 @@ func Directory() (p string, err error) { return } -// Write writes the configuration to the filesystem with the filename and string +// WriteFile writes the configuration to the filesystem with the filename and string func WriteFile(filename string, content []byte) (string, error) { dir, err := Directory() if err != nil { @@ -90,7 +90,7 @@ func Load() (*Config, error) { slog.Debug("Error reading config file", "file", p, "error", err) return nil, err } - utils.Verbosef("Reading config file %s", p) + utilsx.Verbosef("Reading config file %s", p) // If a v1 config is found, migrate it to a v2 one if that is empty hasV1 := v.ConfigV1 != nil && v.ConfigV1.UUID != "" hasV2 := v.Config != nil && len(v.Config.UUIDs) > 0 diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4b5585d..89ac31d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -6,7 +6,7 @@ import ( "reflect" "testing" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" ) func mockDir(t *testing.T, dir string) { @@ -77,7 +77,7 @@ func TestLoad(t *testing.T) { // mock the config name configName = curr.filename gotc, goterr := Load() - if utils.ErrorString(goterr) != curr.wanterr { + if utilsx.ErrorString(goterr) != curr.wanterr { t.Fatalf("expected config error not equal to want, got: %v, want: %v", goterr, curr.wanterr) } // to compare structs we can use deepequal diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 545711e..eb34ed4 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -1,4 +1,4 @@ -// package discovery contains methods to parse the discovery format from https://discovery.eduroam.app/v3/discovery.json into providers +// Package discovery contains methods to parse the discovery format from https://discovery.eduroam.app/v3/discovery.json into providers package discovery import ( @@ -19,6 +19,7 @@ type Discovery struct { Value Value `json:"http://letswifi.app/discovery#v3"` } +// Value is the discovery JSON value type Value struct { Providers provider.Providers `json:"providers"` // See: https://github.com/geteduroam/windows-app/blob/22cd90f36031907c7174fbdc678edafaa627ce49/CHANGELOG.md#changed @@ -68,7 +69,7 @@ func (c *Cache) Providers() (*provider.Providers, error) { slog.Debug("Error requesting discovery.json", "error", err) return &c.Cached.Value.Providers, err } - defer res.Body.Close() + defer res.Body.Close() //nolint:errcheck body, err := io.ReadAll(res.Body) if err != nil { diff --git a/internal/eap/eap.go b/internal/eap/eap.go index e0ae250..55ade96 100644 --- a/internal/eap/eap.go +++ b/internal/eap/eap.go @@ -1,4 +1,4 @@ -// package eap implements an XML eap-config parser compliant with the XML schema found at https://github.com/GEANT/CAT/blob/master/devices/eap_config/eap-metadata.xsd +// Package eap implements an XML eap-config parser compliant with the XML schema found at https://github.com/GEANT/CAT/blob/master/devices/eap_config/eap-metadata.xsd // A part of this was generated with xgen https://github.com/xuri/xgen // By hand modified: // - Use NonEAPAuthNumbers as alias instead of hardcoded int @@ -338,8 +338,8 @@ func (ca *CertData) isValid(format string) bool { return true } -// CaList gets a list of certificates by looping through the certificate list and returning all *valid* certificates -func (ss *ServerCredentialVariants) CAList() ([]byte, error) { +// CAList gets a list of certificates by looping through the certificate list and returning all *valid* certificates +func (ss *ServerCredentialVariants) CAList() (cert.Certificates, error) { var certs []string for _, c := range ss.CA { if c.isValid("X.509") { diff --git a/internal/eap/eap_test.go b/internal/eap/eap_test.go index d23ac6b..5edfd0f 100644 --- a/internal/eap/eap_test.go +++ b/internal/eap/eap_test.go @@ -10,7 +10,7 @@ import ( "github.com/geteduroam/linux-app/internal/network/cert" "github.com/geteduroam/linux-app/internal/network/inner" "github.com/geteduroam/linux-app/internal/network/method" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" ) type authMethodTest struct { @@ -30,7 +30,7 @@ func testAuthMethod(t *testing.T, eip *EAPIdentityProvider, cases []authMethodTe if r != c.want { t.Fatalf("method is not what is expected, got: %d, want: %d", r, c.want) } - if utils.ErrorString(err) != c.err { + if utilsx.ErrorString(err) != c.err { t.Fatalf("error is not expected, got: %v, want: %v", err, c.err) } } @@ -53,7 +53,7 @@ func testSSIDSettings(t *testing.T, eip *EAPIdentityProvider, settings ssidSetti if !reflect.DeepEqual(gotSSIDs, settings.SSIDs) { t.Fatalf("SSIDs is not equal, got: %v, want: %v", gotSSIDs, settings.SSIDs) } - gotErrS := utils.ErrorString(gotErr) + gotErrS := utilsx.ErrorString(gotErr) if gotErrS != settings.err { t.Fatalf("Error for SSID settings is not equal, got: %v, want: %v", gotErrS, settings.err) } @@ -72,7 +72,7 @@ type parseTest struct { netTest networkTest } -func mustParseCert(t *testing.T, root string) []byte { +func mustParseCert(t *testing.T, root string) cert.Certificates { c, err := cert.New([]string{root}) if err != nil { t.Fatalf("failed to generate certs: %v", err) @@ -196,7 +196,7 @@ func TestParse(t *testing.T) { // finally test the whole network we get back n, err := eipl.Network() - errS := utils.ErrorString(err) + errS := utilsx.ErrorString(err) if errS != c.netTest.err { t.Fatalf("network error not equal. Got: %v, want: %v", errS, c.netTest.err) } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 8bcc2ca..df2bfda 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,4 +1,4 @@ -// package handlers handles the eduroam connection by parsing the byte array +// Package handler handles the eduroam connection by parsing the byte array // It has handlers for UI events package handler diff --git a/internal/log/log.go b/internal/logwrap/logwrap.go similarity index 81% rename from internal/log/log.go rename to internal/logwrap/logwrap.go index d0db405..c8fe51a 100644 --- a/internal/log/log.go +++ b/internal/logwrap/logwrap.go @@ -1,4 +1,5 @@ -package log +// Package logwrap implements wrapper around loggers +package logwrap import ( "fmt" @@ -6,10 +7,12 @@ import ( "path/filepath" "github.com/geteduroam/linux-app/internal/config" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "golang.org/x/exp/slog" ) +// Location returns the location of the log file +// and creates it if not exists func Location(program string) (string, error) { logfile := fmt.Sprintf("%s.log", program) dir, err := config.Directory() @@ -47,14 +50,14 @@ func Initialize(program string, debug bool) { if debug { fmt.Printf("Writing debug logs to %s\n", fpath) } else { - utils.Verbosef("Writing logs to %s", fpath) + utilsx.Verbosef("Writing logs to %s", fpath) } } else { slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, opts))) if debug { fmt.Println("Writing debug logs to console, due to error: ", err) } else { - utils.Verbosef("Writing logs to console, due to error: ", err) + utilsx.Verbosef("Writing logs to console, due to error: ", err) } } if debug { diff --git a/internal/network/cert/cert.go b/internal/network/cert/cert.go index 25aa267..8f1d20e 100644 --- a/internal/network/cert/cert.go +++ b/internal/network/cert/cert.go @@ -1,3 +1,4 @@ +// Package cert implements parsing of X509 certificates package cert import ( @@ -5,30 +6,79 @@ import ( "encoding/base64" "encoding/pem" "errors" - "log/slog" + "fmt" + "os" + "path/filepath" + + "github.com/geteduroam/linux-app/internal/network/cert/rehash" ) -// toPEM converts an x509 certificate to a PEM encoded block -func toPEM(cert *x509.Certificate) []byte { - return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) +// toPEMFile converts an x509 certificate `cert` to a PEM encoded file `p` +func toPEMFile(p string, cert *x509.Certificate) error { + gfile, err := os.Create(p) + if err != nil { + return err + } + defer gfile.Close() //nolint:errcheck + return pem.Encode(gfile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) +} + +func removeIfExists(p string) error { + if _, err := os.Stat(p); err == nil { + if err := os.RemoveAll(p); err != nil { + return err + } + } + return nil +} + +// Certificates is a list of x509 Certificates +type Certificates []*x509.Certificate + +// ToDir outputs certificates into a base directory +func (c Certificates) ToDir(baseDir string) error { + caDir := filepath.Join(baseDir, "ca") + // remove the previous CA directory + if err := removeIfExists(caDir); err != nil { + return err + } + // remove a ca-cert.pem in the base dir which is from an old version of the client + if err := removeIfExists(filepath.Join(baseDir, "ca-cert.pem")); err != nil { + return err + } + // make sure the CA dir exists + if err := os.MkdirAll(caDir, 0o700); err != nil { + return err + } + hashes := map[uint32][]string{} + for k, v := range c { + fp := filepath.Join(caDir, fmt.Sprintf("%d.pem", k)) + if err := toPEMFile(fp, v); err != nil { + return err + } + hash, err := rehash.SubjectNameHash(v) + if err != nil { + return fmt.Errorf("error getting subject name hash for cert (path=%q)\n%w", fp, err) + } + hashes[hash] = append(hashes[hash], fp) + } + return rehash.CreateSymlinks(caDir, hashes) } // New creates certs by decoding each certificate and continuing on invalid certificates // It returns PEM encoded data -func New(data []string) ([]byte, error) { - var ret []byte - for _, v := range data { +func New(data []string) (Certificates, error) { + ret := make(Certificates, len(data)) + for i, v := range data { b, err := base64.StdEncoding.DecodeString(v) if err != nil { - slog.Error("failed parsing certificate", "error", err) - continue + return nil, fmt.Errorf("failed decoding base64 for certificate: %w", err) } cert, err := x509.ParseCertificate(b) if err != nil { - slog.Error("failed parsing certificate", "error", err) - continue + return nil, fmt.Errorf("failed parsing certificate: %w", err) } - ret = append(ret, toPEM(cert)...) + ret[i] = cert } if len(data) == 0 { diff --git a/internal/network/cert/clientcert.go b/internal/network/cert/clientcert.go index a2e5439..b104f9c 100644 --- a/internal/network/cert/clientcert.go +++ b/internal/network/cert/clientcert.go @@ -90,6 +90,11 @@ func (cc *ClientCert) PrivateKeyPEMEnc() (pemb []byte, pwd string, err error) { return pem.EncodeToMemory(block), pwd, nil } +// toPEM converts an x509 certificate to a PEM encoded block +func toPEM(cert *x509.Certificate) []byte { + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) +} + // ToPEM generates the PEM bytes for the client certificate func (cc *ClientCert) ToPEM() []byte { return toPEM(cc.cert) diff --git a/internal/network/cert/rehash/rehash.go b/internal/network/cert/rehash/rehash.go new file mode 100644 index 0000000..58a2326 --- /dev/null +++ b/internal/network/cert/rehash/rehash.go @@ -0,0 +1,340 @@ +// Package rehash reimplements parts of "openssl rehash" +// copied from: https://github.com/paketo-buildpacks/ca-certificates/blob/4f8433e850a61e43b8f28743d813249a818ab0ff/cacerts/certs.go +// We have copied only parts of this file and made modifications indicated with "MODIFIED" in this file +// NOTICE of the project: +/* + * ca-certificates + * + * Copyright (c) 2020-Present CloudFoundry.org Foundation, Inc. All Rights Reserved. + * + * This project is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this project except in compliance with the License. + * + * This project may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ +// LICENSE of the project: +/* + * + * Apache License + * Version 2.0, January 2004 + * https://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 + * + * https://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. + */ +// LICENSE of the file: +/* + * Copyright 2018-2024 the original author or authors. + * + * 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 + * + * https://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. + */ +package rehash + +import ( + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// canonicalSET holds a of canonicalATVs. Suffix SET ensures it is marshaled as a set rather than a sequence +// by asn1.Marshal. +type canonicalSET []canonicalATV + +// canonicalATV is similar to pkix.AttributeTypeAndValue but includes tag to ensure all values are marshaled as +// ASN.1, UTF8String values +type canonicalATV struct { + Type asn1.ObjectIdentifier + Value string `asn1:"utf8"` +} + +// CanonicalString transforms the given string. All leading and trailing whitespace is trimmed +// where whitespace is defined as a space, formfeed, tab, newline, carriage return, or vertical tab +// character. Any remaining sequence of one or more consecutive whitespace characters in replaced with +// a single ' '. +// +// This is a reimplementation of the asn1_string_canon in openssl +func CanonicalString(s string) string { + s = strings.TrimLeft(s, " \f\t\n\v") + s = strings.TrimRight(s, " \f\t\n\v") + s = strings.ToLower(s) + return string(regexp.MustCompile(`[[:space:]]+`).ReplaceAll([]byte(s), []byte(" "))) +} + +// canonicalName accepts a DER encoded subject name and returns a "Canonical Encoding" matching that +// returned by the x509_name_canon function in openssl. All string values are transformed with CanonicalString +// and UTF8 encoded and the leading SEQ header is removed. +// +// For more information see https://stackoverflow.com/questions/34095440/hash-algorithm-for-certificate-crl-directory. +func canonicalName(name []byte) ([]byte, error) { + var origSeq pkix.RDNSequence + _, err := asn1.Unmarshal(name, &origSeq) + if err != nil { + return nil, fmt.Errorf("failed to parse subject name\n%w", err) + } + var result []byte + for _, origSet := range origSeq { + var canonSet canonicalSET + for _, origATV := range origSet { + origVal, ok := origATV.Value.(string) + if !ok { + return nil, errors.New("got unexpected non-string value") + } + canonSet = append(canonSet, canonicalATV{ + Type: origATV.Type, + Value: CanonicalString(origVal), + }) + } + setBytes, err := asn1.Marshal(canonSet) + if err != nil { + return nil, fmt.Errorf("failed to marshal canonical name\n%w", err) + } + result = append(result, setBytes...) + } + return result, nil +} + +// SubjectNameHash is a reimplementation of the X509_subject_name_hash in openssl. It computes the SHA-1 +// of the canonical encoding of the certificate's subject name and returns the 32-bit integer represented by the first +// four bytes of the hash using little-endian byte order. +func SubjectNameHash(cert *x509.Certificate) (uint32, error) { + name, err := canonicalName(cert.RawSubject) + if err != nil { + return 0, fmt.Errorf("failed to compute canonical subject name\n%w", err) + } + hasher := sha1.New() + _, err = hasher.Write(name) + if err != nil { + return 0, fmt.Errorf("failed to compute sha1sum of canonical subject name\n%w", err) + } + sum := hasher.Sum(nil) + return binary.LittleEndian.Uint32(sum[:4]), nil +} + +// CreateSymlinks creates symlinks for the hashes in the caDir +// MODIFIED FROM: https://github.com/paketo-buildpacks/ca-certificates/blob/4f8433e850a61e43b8f28743d813249a818ab0ff/cacerts/certs.go#L72-L79 +func CreateSymlinks(caDir string, hashes map[uint32][]string) error { + for hash, paths := range hashes { + for i, path := range paths { + name := fmt.Sprintf("%08x.%d", hash, i) + if err := os.Symlink(path, filepath.Join(caDir, name)); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/network/inner/inner.go b/internal/network/inner/inner.go index e200636..809d5d8 100644 --- a/internal/network/inner/inner.go +++ b/internal/network/inner/inner.go @@ -1,3 +1,4 @@ +// Package inner implements inner EAP authentication methods package inner import ( @@ -9,13 +10,19 @@ type Type int // TODO: Should we split these in EAP and non-EAP instead? const ( - None Type = 0 - Pap Type = 1 - Mschap Type = 2 + // None is no EAP inner authentication + None Type = 0 + // Pap is PAP inner authentication + Pap Type = 1 + // Mschap is MSCHAP inner authentication + Mschap Type = 2 + // Mschapv2 is MSCHAPv2 inner authentication Mschapv2 Type = 3 // TODO: remove this? https://github.com/geteduroam/windows-app/blob/f11f00dee3eb71abd38537e18881463f83b180d3/CHANGELOG.md?plain=1#L34 + // EapPeapMschapv2 is EAP-PEAP-MSCHAPv2 inner authentication EapPeapMschapv2 Type = 25 - EapMschapv2 Type = 26 + // EapMschapv2 is EAP-MSCHAPv2 inner authentication + EapMschapv2 Type = 26 ) // EAP returns whether the type is an EAP inner type diff --git a/internal/network/method/method.go b/internal/network/method/method.go index 17d79c6..5850593 100644 --- a/internal/network/method/method.go +++ b/internal/network/method/method.go @@ -1,11 +1,15 @@ +// Package method implements EAP methods package method // Type defines the EAP methods that are returned by the EAP xml type Type int const ( - TLS Type = 13 + // TLS is the TLS EAP Method + TLS Type = 13 + // TTLS is the TTLS EAP Method TTLS Type = 21 + // PEAP is the PEAP EAP Method PEAP Type = 25 ) diff --git a/internal/network/network.go b/internal/network/network.go index 45ac2cc..159c639 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -1,3 +1,4 @@ +// Package network implements functions for configuring an eduroam network package network import ( @@ -8,6 +9,7 @@ import ( "github.com/geteduroam/linux-app/internal/network/method" ) +// Network defines the interface for a network // A network belongs to the network interface when it has a method type Network interface { // Method returns the EAP method @@ -51,7 +53,7 @@ type SSID struct { // Base is the definition that each network always has type Base struct { // Certs is the list of CA certificates that are used - Certs []byte + Certs cert.Certificates // SSIDs are the list of SSIDs SSIDs []SSID // ServerIDs is the list of server names @@ -89,10 +91,12 @@ type NonTLS struct { InnerAuth inner.Type } +// Method returns the method for the NonTLS network func (n *NonTLS) Method() method.Type { return n.MethodType } +// ProviderInfo returns the provider info for the NonTLS network func (n *NonTLS) ProviderInfo() ProviderInfo { return n.Base.ProviderInfo } @@ -110,14 +114,17 @@ type TLS struct { Password string } +// Method returns the method for the TLS network func (t *TLS) Method() method.Type { return method.TLS } +// ProviderInfo returns the provider info for the TLS network func (t *TLS) ProviderInfo() ProviderInfo { return t.Base.ProviderInfo } +// Validity returns the start time and expiry time of the TLS client certificate func (t *TLS) Validity() (time.Time, time.Time) { return t.ClientCert.Validity() } diff --git a/internal/nm/base/base.go b/internal/nm/base/base.go index 15668af..268e494 100644 --- a/internal/nm/base/base.go +++ b/internal/nm/base/base.go @@ -1,3 +1,4 @@ +// Package base implements the base of the DBUS connection with NetworkManager package base import ( @@ -5,15 +6,19 @@ import ( ) const ( - Interface = "org.freedesktop.NetworkManager" + // Interface is the DBUS interface for NetworkManager + Interface = "org.freedesktop.NetworkManager" + // ObjectPath is the DBUS Object Path for NetworkManager ObjectPath = "/org/freedesktop/NetworkManager" ) +// Base is the base DBUS connection for NetworkManager type Base struct { conn *dbus.Conn object dbus.BusObject } +// Init initializes NetworkManager dbus connection func (b *Base) Init(iface string, objectPath dbus.ObjectPath) error { var err error @@ -27,14 +32,17 @@ func (b *Base) Init(iface string, objectPath dbus.ObjectPath) error { return nil } +// Call calls a DBUS method func (b *Base) Call(method string, args ...interface{}) error { return b.object.Call(method, 0, args...).Err } +// CallReturn calls a DBUS method and returns data func (b *Base) CallReturn(ret interface{}, method string, args ...interface{}) error { return b.object.Call(method, 0, args...).Store(ret) } +// Path returns the DBUS object path func (b *Base) Path() dbus.ObjectPath { return b.object.Path() } diff --git a/internal/nm/connection/connection.go b/internal/nm/connection/connection.go index a6cd088..52064db 100644 --- a/internal/nm/connection/connection.go +++ b/internal/nm/connection/connection.go @@ -1,3 +1,4 @@ +// Package connection implements the NetworkManager connection DBUS interface package connection import ( @@ -8,16 +9,22 @@ import ( ) const ( - Interface = SettingsInterface + ".Connection" - Delete = Interface + ".Delete" - Update = Interface + ".Update" + // Interface is the interface for a DBUS connection + Interface = SettingsInterface + ".Connection" + // Delete is the interface for the method to delete a DBUS connection + Delete = Interface + ".Delete" + // Update is the interface for the method to update a DBUS connection + Update = Interface + ".Update" + // GetSettings is the interface for the method to get settings for a DBUS connection GetSettings = Interface + ".GetSettings" ) +// Connection is a NetworkManager connection type Connection struct { base.Base } +// New creates a new NetworkManager DBUS connection func New(path dbus.ObjectPath) (*Connection, error) { c := &Connection{} err := c.Init(base.Interface, path) @@ -28,14 +35,17 @@ func New(path dbus.ObjectPath) (*Connection, error) { return c, nil } +// Update updates the connection func (c *Connection) Update(settings SettingsArgs) error { return c.Call(Update, settings) } +// Delete deletes the connection func (c *Connection) Delete() error { return c.Call(Delete) } +// GetSettings gets settings for the connection func (c *Connection) GetSettings() (SettingsArgs, error) { var settings map[string]map[string]dbus.Variant diff --git a/internal/nm/connection/settings.go b/internal/nm/connection/settings.go index 81c6d0a..4b85ff6 100644 --- a/internal/nm/connection/settings.go +++ b/internal/nm/connection/settings.go @@ -9,15 +9,21 @@ import ( ) const ( - SettingsInterface = base.Interface + ".Settings" + // SettingsInterface is the interface to get the settings + SettingsInterface = base.Interface + ".Settings" + // SettingsObjectPath is the path for the settings SettingsObjectPath = base.ObjectPath + "/Settings" - SettingsAddConnection = SettingsInterface + ".AddConnection" + // SettingsAddConnection is the interface to add a connection + SettingsAddConnection = SettingsInterface + ".AddConnection" + // SettingsGetConnectionByUUID is the interface to get a connection by UUID SettingsGetConnectionByUUID = SettingsInterface + ".GetConnectionByUuid" ) +// SettingsArgs is the arguments for connection settings type SettingsArgs map[string]map[string]interface{} +// UUID returns the UUID for the connection func (s SettingsArgs) UUID() (string, error) { c, ok := s["connection"] if !ok { @@ -34,6 +40,7 @@ func (s SettingsArgs) UUID() (string, error) { return uuidS, nil } +// SSID returns the SSID for the connection func (s SettingsArgs) SSID() (string, error) { c, ok := s["802-11-wireless"] if !ok { @@ -50,10 +57,12 @@ func (s SettingsArgs) SSID() (string, error) { return string(ssidS), nil } +// Settings returns the settings for the connection type Settings struct { base.Base } +// NewSettings creates new connection settings func NewSettings() (*Settings, error) { s := &Settings{} err := s.Init(base.Interface, SettingsObjectPath) @@ -63,6 +72,7 @@ func NewSettings() (*Settings, error) { return s, nil } +// AddConnection adds a connection for the settings func (s *Settings) AddConnection(settings SettingsArgs) (*Connection, error) { var path dbus.ObjectPath err := s.CallReturn(&path, SettingsAddConnection, settings) @@ -73,6 +83,7 @@ func (s *Settings) AddConnection(settings SettingsArgs) (*Connection, error) { return New(path) } +// ConnectionByUUID gets a connection by UUID for the settings func (s *Settings) ConnectionByUUID(uuid string) (*Connection, error) { var path dbus.ObjectPath err := s.CallReturn(&path, SettingsGetConnectionByUUID, uuid) diff --git a/internal/nm/nm.go b/internal/nm/nm.go index a774dc9..9e56f70 100644 --- a/internal/nm/nm.go +++ b/internal/nm/nm.go @@ -1,9 +1,11 @@ +// Package nm implements the functions needed to configure the connection using NetworkManager package nm import ( "errors" "fmt" "os/user" + "path/filepath" "slices" "strings" @@ -112,12 +114,16 @@ func installBaseSSID(n network.Base, ssid network.SSID, specifics map[string]int v := fmt.Sprintf("DNS:%s", sid) sids = append(sids, v) } - caFile, err := encodeFileBytes("ca-cert.pem", n.Certs) + caBasePath, err := config.Directory() + if err != nil { + return "", err + } + err = n.Certs.ToDir(caBasePath) if err != nil { return "", err } s8021x := map[string]interface{}{ - "ca-cert": caFile, + "ca-path": filepath.Join(caBasePath, "ca"), "altsubject-matches": sids, } // add the network specific settings diff --git a/internal/notification/notification.go b/internal/notification/notification.go index ad8ed5c..2e7bfc2 100644 --- a/internal/notification/notification.go +++ b/internal/notification/notification.go @@ -1,3 +1,5 @@ +// Package notification implements notifications for the connection +// To show when it's about to expire package notification import ( diff --git a/internal/notification/systemd/systemd.go b/internal/notification/systemd/systemd.go index bb57949..f76f483 100644 --- a/internal/notification/systemd/systemd.go +++ b/internal/notification/systemd/systemd.go @@ -1,3 +1,4 @@ +// Package systemd implements notification support with systemd package systemd import ( diff --git a/internal/provider/profile.go b/internal/provider/profile.go index 9ef230d..595b9c0 100644 --- a/internal/provider/profile.go +++ b/internal/provider/profile.go @@ -73,7 +73,7 @@ func (p *Profile) RedirectURI() (string, error) { // readResponse reads the HTTP response and returns the body and error // It also ensures the body is closed in the end to prevent a resource leak func readResponse(res *http.Response) ([]byte, error) { - defer res.Body.Close() + defer res.Body.Close() //nolint:errcheck body, err := io.ReadAll(res.Body) if err != nil { return nil, err diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ef5c5f2..4bfcd96 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1,3 +1,4 @@ +// Package provider implements various functions for a discovery provider package provider import ( @@ -11,17 +12,22 @@ import ( "strings" "time" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "golang.org/x/text/language" ) +// LocalizedString is a string localized in the language type LocalizedString struct { + // Display is the string that is displayed Display string `json:"display"` - Lang string `json:"lang"` + // Lang is the language for the display + Lang string `json:"lang"` } +// LocalizedStrings is a list of localized strings type LocalizedStrings []LocalizedString +// Corpus returns the localized strings joined in one single search string func (ls LocalizedStrings) Corpus() string { var corpus strings.Builder for _, v := range ls { @@ -46,6 +52,7 @@ func setSystemLanguage() { systemLanguage = tag } +// Get gets a string based on the system language func (ls LocalizedStrings) Get() string { // first get the non-empty values var disp string @@ -77,15 +84,22 @@ func (ls LocalizedStrings) Get() string { return disp } +// Provider is the info for a single eduroam/getgovroam etc provider type Provider struct { - ID string `json:"id"` - Country string `json:"country"` - Name LocalizedStrings `json:"name"` - Profiles []Profile `json:"profiles"` + // ID is the identifier of the provider + ID string `json:"id"` + // Country is where the provider is from + Country string `json:"country"` + // Name is the name of the provider + Name LocalizedStrings `json:"name"` + // Profiles is the list of profiles for the provider + Profiles []Profile `json:"profiles"` } +// Providers is the list of providers type Providers []Provider +// SortNames sorts two localized strings func SortNames(a LocalizedStrings, b LocalizedStrings, search string) int { la := strings.ToLower(a.Corpus()) lb := strings.ToLower(b.Corpus()) @@ -109,22 +123,31 @@ func SortNames(a LocalizedStrings, b LocalizedStrings, search string) int { return 1 } +// ByName is the struct that implements by name sorting type ByName struct { + // Providers is the list of providers Providers Providers - Search string + // Search is the search string + Search string } -func (s ByName) Len() int { return len(s.Providers) } +// Len returns the length of the ByName sort +func (s ByName) Len() int { return len(s.Providers) } + +// Swap swaps two providers func (s ByName) Swap(i, j int) { s.Providers[i], s.Providers[j] = s.Providers[j], s.Providers[i] } + +// Less sorts the providers func (s ByName) Less(i, j int) bool { diff := SortNames(s.Providers[i].Name, s.Providers[j].Name, s.Search) // if i is less than j, diff returns less than 0 return diff < 0 } +// FilterSingle searches inside the corpus func FilterSingle(name LocalizedStrings, search string) bool { - l1, err1 := utils.RemoveDiacritics(strings.ToLower(name.Corpus())) - l2, err2 := utils.RemoveDiacritics(strings.ToLower(search)) + l1, err1 := utilsx.RemoveDiacritics(strings.ToLower(name.Corpus())) + l2, err2 := utilsx.RemoveDiacritics(strings.ToLower(search)) if err1 != nil || err2 != nil { return false } @@ -150,6 +173,7 @@ func (i *Providers) FilterSort(search string) *Providers { return &x.Providers } +// Custom gets provider info using a custom URL func Custom(ctx context.Context, query string) (*Provider, error) { client := http.Client{Timeout: 10 * time.Second} // parse URL and add scheme diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 2bcbd1e..1a63967 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -3,7 +3,7 @@ package provider import ( "testing" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "golang.org/x/text/language" ) @@ -185,7 +185,7 @@ func TestRedirectURI(t *testing.T) { for _, c := range cases { p.WebviewEndpoint = c.input r, e := p.RedirectURI() - es := utils.ErrorString(e) + es := utilsx.ErrorString(e) if r != c.want || es != c.e { t.Fatalf("Result: %s, %s Want: %s, %s", r, es, c.want, c.e) } diff --git a/internal/utils/utils.go b/internal/utilsx/utilsx.go similarity index 83% rename from internal/utils/utils.go rename to internal/utilsx/utilsx.go index bddbd7b..36eda2c 100644 --- a/internal/utils/utils.go +++ b/internal/utilsx/utilsx.go @@ -1,4 +1,8 @@ -package utils +// Package utilsx implements various utility functions +// the suffix x is needed to make revive linter quiet for a useless package name +// the obvious TODO is then: +// TODO: make this package indeed obsolete +package utilsx import ( "fmt" @@ -32,9 +36,10 @@ func ErrorString(e error) string { return e.Error() } +// IsVerbose returns true if messages should be logged verbosed var IsVerbose bool -// Conditionally (format) print verbose messages +// Verbosef conditionally (format) print verbose messages func Verbosef(msg string, args ...any) { if IsVerbose { fmt.Printf(msg+"\n", args...) diff --git a/internal/utils/utils_test.go b/internal/utilsx/utilsx_test.go similarity index 96% rename from internal/utils/utils_test.go rename to internal/utilsx/utilsx_test.go index 2dfa6b4..1720c66 100644 --- a/internal/utils/utils_test.go +++ b/internal/utilsx/utilsx_test.go @@ -1,4 +1,4 @@ -package utils +package utilsx import "testing" diff --git a/internal/variant/geteduroam.go b/internal/variant/geteduroam.go index 624dea2..e36a637 100644 --- a/internal/variant/geteduroam.go +++ b/internal/variant/geteduroam.go @@ -1,10 +1,15 @@ //go:build !getgovroam +// Package variant implements settings for different geteduroam variants package variant const ( - AppID string = "app.eduroam.geteduroam" + // AppID is the application ID for geteduroam + AppID string = "app.eduroam.geteduroam" + // DiscoveryURL is the discovery URL for geteduroam DiscoveryURL string = "https://discovery.eduroam.app/v3/discovery.json" - DisplayName string = "geteduroam" - ProfileName string = "eduroam" + // DisplayName is the display name for geteduroam + DisplayName string = "geteduroam" + // ProfileName is the connection profile name for geteduroam + ProfileName string = "eduroam" ) diff --git a/internal/variant/getgovroam.go b/internal/variant/getgovroam.go index 97a4ed6..b851bbb 100644 --- a/internal/variant/getgovroam.go +++ b/internal/variant/getgovroam.go @@ -3,8 +3,12 @@ package variant const ( - AppID string = "nl.govroam.getgovroam" + // AppID is the application ID for getgovroam + AppID string = "nl.govroam.getgovroam" + // DiscoveryURL is the discovery URL for getgovroam DiscoveryURL string = "https://discovery.getgovroam.nl/v3/discovery.json" - DisplayName string = "getgovroam" - ProfileName string = "govroam" + // DisplayName is the display name for getgovroam + DisplayName string = "getgovroam" + // ProfileName is the connection profile name for govroam + ProfileName string = "govroam" )