From 925b66adb2492c75293e33e611aca10eb85aa9ea Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Thu, 12 Feb 2026 17:13:13 +0100 Subject: [PATCH 1/8] Cert + NM: Use ca-path instead of ca-cert This is due to: https://github.com/geteduroam/linux-app/issues/82 And: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/1886 A recent change in NetworkManager makes ca-certs passed as blobs to wpa-supplicant when using connections that are non-system wide (only for the current user in our case). This makes our use case not work as eduroam can have multiple CA certificates. Make sure to pass it as ca-path. It also includes other fixes in eap to be more strict when parsing X509 certificates. Instead of skipping over invalid certificates it shows an error and fails to continue. --- internal/eap/eap.go | 2 +- internal/network/cert/cert.go | 67 +++++++++++++++++++++++------ internal/network/cert/clientcert.go | 5 +++ internal/network/network.go | 2 +- internal/nm/nm.go | 9 +++- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/internal/eap/eap.go b/internal/eap/eap.go index e0ae250..755c57a 100644 --- a/internal/eap/eap.go +++ b/internal/eap/eap.go @@ -339,7 +339,7 @@ func (ca *CertData) isValid(format string) bool { } // CaList gets a list of certificates by looping through the certificate list and returning all *valid* certificates -func (ss *ServerCredentialVariants) CAList() ([]byte, error) { +func (ss *ServerCredentialVariants) CAList() (cert.Certificates, error) { var certs []string for _, c := range ss.CA { if c.isValid("X.509") { diff --git a/internal/network/cert/cert.go b/internal/network/cert/cert.go index 25aa267..e916887 100644 --- a/internal/network/cert/cert.go +++ b/internal/network/cert/cert.go @@ -5,30 +5,73 @@ import ( "encoding/base64" "encoding/pem" "errors" - "log/slog" + "fmt" + "os" + "path/filepath" ) -// 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() + pem.Encode(gfile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) + return nil +} + +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 + } + for k, v := range c { + fp := filepath.Join(caDir, fmt.Sprintf("%d.pem", k)) + if err := toPEMFile(fp, v); err != nil { + return err + } + } + // TODO: rehash dir + return nil } // 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/network.go b/internal/network/network.go index 45ac2cc..40a86bd 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -51,7 +51,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 diff --git a/internal/nm/nm.go b/internal/nm/nm.go index a774dc9..12f64e2 100644 --- a/internal/nm/nm.go +++ b/internal/nm/nm.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os/user" + "path/filepath" "slices" "strings" @@ -112,12 +113,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 From 373f126bd2e424b17b2a213c56296c4e71901d29 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Thu, 12 Feb 2026 18:12:46 +0100 Subject: [PATCH 2/8] Cert: Add rehash implementation We need to rehash the ca certificates dir. We can shell out to OpenSSL, but then we need to depend on OpenSSL at runtime. Let's copy a rehash implementation from another project and show the necessary licenses and notices. This implementation seems to work well. --- internal/network/cert/cert.go | 11 +- internal/network/cert/rehash/rehash.go | 340 +++++++++++++++++++++++++ 2 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 internal/network/cert/rehash/rehash.go diff --git a/internal/network/cert/cert.go b/internal/network/cert/cert.go index e916887..65feea4 100644 --- a/internal/network/cert/cert.go +++ b/internal/network/cert/cert.go @@ -8,6 +8,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/geteduroam/linux-app/internal/network/cert/rehash" ) // toPEMFile converts an x509 certificate `cert` to a PEM encoded file `p` @@ -48,14 +50,19 @@ func (c Certificates) ToDir(baseDir string) error { 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) } - // TODO: rehash dir - return nil + return rehash.CreateSymlinks(caDir, hashes) } // New creates certs by decoding each certificate and continuing on invalid certificates 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 +} From 3a27e7b04840f2d9d0a6a2b89ca68b0bb332b869 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 11:39:30 +0100 Subject: [PATCH 3/8] EAP Test: Fix certificates type --- internal/eap/eap_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/eap/eap_test.go b/internal/eap/eap_test.go index d23ac6b..e5c612d 100644 --- a/internal/eap/eap_test.go +++ b/internal/eap/eap_test.go @@ -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) From 3331baa3c9a1a2d7ac4e1d35aff800efbec297e6 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 11:43:24 +0100 Subject: [PATCH 4/8] Github workflow: Upgrade golangci-lint action --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5022986..43c7b64 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 stylecheck,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 From 39e9d447993ea1f7a33f508c6d0f34bca8922a33 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 12:02:06 +0100 Subject: [PATCH 5/8] Test: use staticcheck for goreleaser --- .github/workflows/goreleaser.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 43c7b64..029a751 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: # See https://github.com/golangci/golangci-lint-action/issues/485 for why we do --out-.... with: version: latest - args: "-E stylecheck,revive,gocritic --timeout 5m" + args: "-E staticcheck,revive,gocritic --timeout 5m" build-cli: name: Build CLI From 21c4123dd95cf441797af5dfb3a02d7ea44e2560 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 12:31:48 +0100 Subject: [PATCH 6/8] All: add missing comments --- cmd/geteduroam-gui/util.go | 2 +- internal/config/config.go | 4 +-- internal/discovery/discovery.go | 5 ++-- internal/eap/eap.go | 4 +-- internal/handler/handler.go | 2 +- internal/log/log.go | 3 ++ internal/network/cert/cert.go | 6 ++-- internal/network/inner/inner.go | 15 +++++++--- internal/network/method/method.go | 6 +++- internal/network/network.go | 6 ++++ internal/nm/base/base.go | 10 ++++++- internal/nm/connection/connection.go | 16 ++++++++-- internal/nm/connection/settings.go | 15 ++++++++-- internal/notification/notification.go | 2 ++ internal/notification/systemd/systemd.go | 1 + internal/provider/profile.go | 2 +- internal/provider/provider.go | 37 +++++++++++++++++++----- internal/utils/utils.go | 3 +- internal/variant/geteduroam.go | 11 +++++-- internal/variant/getgovroam.go | 10 +++++-- internal/version/version.go | 1 + 21 files changed, 124 insertions(+), 37 deletions(-) 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/internal/config/config.go b/internal/config/config.go index 21280ca..c389730 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 ( @@ -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 { 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 755c57a..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,7 +338,7 @@ 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 +// 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 { 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/log/log.go index d0db405..ef6c960 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,3 +1,4 @@ +// Package log implements wrapper around loggers package log import ( @@ -10,6 +11,8 @@ import ( "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() diff --git a/internal/network/cert/cert.go b/internal/network/cert/cert.go index 65feea4..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 ( @@ -18,9 +19,8 @@ func toPEMFile(p string, cert *x509.Certificate) error { if err != nil { return err } - defer gfile.Close() - pem.Encode(gfile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) - return nil + defer gfile.Close() //nolint:errcheck + return pem.Encode(gfile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) } func removeIfExists(p string) error { 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 40a86bd..30aba27 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -8,6 +8,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 @@ -89,10 +90,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 +113,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/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..507e976 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -15,13 +15,18 @@ import ( "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 +51,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 +83,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,19 +122,28 @@ 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)) @@ -150,6 +172,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/utils/utils.go b/internal/utils/utils.go index bddbd7b..a27eae8 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -32,9 +32,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/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" ) diff --git a/internal/version/version.go b/internal/version/version.go index d526fed..f440f4e 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,3 +1,4 @@ +// Package version implements version info for the client package version import ( From 94e8b9bd5f559e3cab1cbc169e7184b765f80ffc Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 13:52:06 +0100 Subject: [PATCH 7/8] All: Get rid of useless package names --- cmd/geteduroam-cli/main.go | 18 +++++++++--------- cmd/geteduroam-gui/main.go | 14 +++++++------- cmd/geteduroam-gui/success.go | 8 ++++---- cmd/geteduroam-notifcheck/main.go | 6 +++--- .../version.go => clientver/clientver.go} | 4 ++-- internal/config/config.go | 4 ++-- internal/config/config_test.go | 4 ++-- internal/eap/eap_test.go | 8 ++++---- internal/{log/log.go => logwrap/logwrap.go} | 8 ++++---- internal/provider/provider.go | 6 +++--- internal/provider/provider_test.go | 4 ++-- internal/{utils/utils.go => utilsx/utilsx.go} | 6 +++++- .../utils_test.go => utilsx/utilsx_test.go} | 2 +- 13 files changed, 48 insertions(+), 44 deletions(-) rename internal/{version/version.go => clientver/clientver.go} (89%) rename internal/{log/log.go => logwrap/logwrap.go} (89%) rename internal/{utils/utils.go => utilsx/utilsx.go} (89%) rename internal/{utils/utils_test.go => utilsx/utilsx_test.go} (96%) 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-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 f440f4e..bf3c3b5 100644 --- a/internal/version/version.go +++ b/internal/clientver/clientver.go @@ -1,5 +1,5 @@ -// Package version implements version info for the client -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 c389730..86229b9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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" ) @@ -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/eap/eap_test.go b/internal/eap/eap_test.go index e5c612d..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) } @@ -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/log/log.go b/internal/logwrap/logwrap.go similarity index 89% rename from internal/log/log.go rename to internal/logwrap/logwrap.go index ef6c960..2e9a371 100644 --- a/internal/log/log.go +++ b/internal/logwrap/logwrap.go @@ -1,5 +1,5 @@ // Package log implements wrapper around loggers -package log +package logwrap import ( "fmt" @@ -7,7 +7,7 @@ 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" ) @@ -50,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/provider/provider.go b/internal/provider/provider.go index 507e976..88a049d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/geteduroam/linux-app/internal/utils" + "github.com/geteduroam/linux-app/internal/utilsx" "golang.org/x/text/language" ) @@ -145,8 +145,8 @@ func (s ByName) Less(i, j int) bool { // 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 } 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 89% rename from internal/utils/utils.go rename to internal/utilsx/utilsx.go index a27eae8..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" 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" From 34ad0b4237ee33ce7230f5b8f7da903ad596b563 Mon Sep 17 00:00:00 2001 From: Jeroen Wijenbergh Date: Mon, 16 Feb 2026 13:55:47 +0100 Subject: [PATCH 8/8] All: Add missing package comments --- internal/logwrap/logwrap.go | 2 +- internal/network/network.go | 1 + internal/nm/nm.go | 1 + internal/provider/provider.go | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/logwrap/logwrap.go b/internal/logwrap/logwrap.go index 2e9a371..c8fe51a 100644 --- a/internal/logwrap/logwrap.go +++ b/internal/logwrap/logwrap.go @@ -1,4 +1,4 @@ -// Package log implements wrapper around loggers +// Package logwrap implements wrapper around loggers package logwrap import ( diff --git a/internal/network/network.go b/internal/network/network.go index 30aba27..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 ( diff --git a/internal/nm/nm.go b/internal/nm/nm.go index 12f64e2..9e56f70 100644 --- a/internal/nm/nm.go +++ b/internal/nm/nm.go @@ -1,3 +1,4 @@ +// Package nm implements the functions needed to configure the connection using NetworkManager package nm import ( diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 88a049d..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 (