├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── nightly.yml ├── .gitignore ├── CONTRIBUTING.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── RR ├── ci.sh ├── cmds ├── cli │ ├── ci.json │ ├── main.go │ ├── types.go │ └── utils.go ├── webboot │ ├── Readme.md │ ├── distros.json │ ├── network.go │ ├── testdata │ │ └── dirlevel1 │ │ │ ├── dirlevel2 │ │ │ └── TinyCorePure64.iso │ │ │ └── fakeDistro.iso │ ├── types.go │ ├── utils.go │ ├── webboot.go │ └── webboot_test.go └── wifiDebug │ └── wifiDebug.go ├── config-5.6.14 ├── distros.md ├── docs ├── remaster-arch-iso.md ├── remaster-debian-iso.md └── remaster-manjaro-iso.md ├── firsttime.sh ├── go.mod ├── go.sum ├── integration.sh ├── integration └── basic_test.go ├── makeusb.sh ├── pkg ├── bootiso │ ├── bootiso.go │ ├── bootiso_test.go │ └── testdata │ │ ├── TinyCorePure64.iso │ │ ├── TinyCorePure64.md5.txt │ │ └── TinyCorePure64.sha256.txt ├── dhclient │ └── dhclient.go ├── menu │ ├── menu.go │ └── menu_test.go ├── wifi │ ├── iwl.go │ ├── iwl_test.go │ ├── iwlistStubOutput.txt │ ├── native.go │ ├── native_test.go │ ├── stub.go │ ├── types.go │ └── wireless.go └── wpa │ └── passphrase │ ├── passphrase.go │ └── passphrase_test.go ├── roadmap.md ├── run-webboot.sh ├── syslinux.cfg.example ├── tips.md └── webboot.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # Triggers the workflow on push or pull request events but only for the 4 | # main branch 5 | push: 6 | branches: [main] 7 | pull_request: 8 | branches: [main] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in 14 | # parallel 15 | jobs: 16 | format_and_unit_testing: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Environment variables 21 | env: 22 | # GOPATH is the current directory. 23 | GOPATH: ${{ github.workspace }} 24 | 25 | # Set the working directory to the correct place in $GOPATH. 26 | defaults: 27 | run: 28 | working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 29 | 30 | # Steps represent a sequence of tasks that will be executed as part of the 31 | # job 32 | steps: 33 | - name: Install Go 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: 1.18.x 37 | - name: Checkout code 38 | uses: actions/checkout@v2 39 | with: 40 | path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }} 41 | - name: Checkout dependencies 42 | run: | 43 | sudo apt-get update 44 | ./firsttime.sh 45 | - name: Build and test webboot 46 | run: | 47 | ./ci.sh 48 | 49 | integration_testing: 50 | runs-on: ubuntu-latest 51 | # Environment variables 52 | env: 53 | # GOPATH is the current directory. 54 | GOPATH: ${{ github.workspace }} 55 | # WEBBOOT is the where the code is checked out. 56 | WEBBOOT: ${{ github.workspace }}/src/github.com/${{ github.repository }} 57 | # Set the working directory to the correct place in $GOPATH. 58 | defaults: 59 | run: 60 | working-directory: ${{ env.WEBBOOT }} 61 | strategy: 62 | # Run all distros at the same time. 63 | max-parallel: 10 64 | # Continue testing other distros on failure. 65 | fail-fast: false 66 | # List of distros 67 | matrix: 68 | distro: 69 | - TinyCore 70 | - Arch 71 | - CentOS 7 72 | - Debian 73 | - Fedora 74 | - Kali 75 | - Linux Mint 76 | - Manjaro 77 | - Ubuntu 78 | # Steps represent a sequence of tasks that will be executed as part of the 79 | # job 80 | steps: 81 | - name: Install Go 82 | uses: actions/setup-go@v2 83 | with: 84 | go-version: 1.18.x 85 | - name: Checkout code 86 | uses: actions/checkout@v2 87 | with: 88 | path: ${{ env.WEBBOOT }} 89 | - name: Checkout dependencies 90 | run: | 91 | sudo apt-get update 92 | ./firsttime.sh 93 | - name: Integration testing 94 | run: | 95 | ./integration.sh "${{ matrix.distro }}" 96 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | # Nightly runs integration testing to check for stale distro metadata (hardcoded 2 | # in cmds/cli/ci.json and cmds/webboot/distros.json). 3 | # 4 | # Distros regularly update to new versions, but webboot's distro metadata is 5 | # hardcoded. Although integration testing is triggered on push and pull 6 | # requests, webboot development is not always active. Testing distro metadata 7 | # regularly helps catch out-of-date info sooner. 8 | name: nightly 9 | on: 10 | # Triggers the workflow every day at 23:00 11 | schedule: 12 | - cron: "0 23 * * *" 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | jobs: 18 | integration_testing: 19 | runs-on: ubuntu-latest 20 | # Environment variables 21 | env: 22 | # GOPATH is the current directory. 23 | GOPATH: ${{ github.workspace }} 24 | # WEBBOOT is the where the code is checked out. 25 | WEBBOOT: ${{ github.workspace }}/src/github.com/${{ github.repository }} 26 | # Set the working directory to the correct place in $GOPATH. 27 | defaults: 28 | run: 29 | working-directory: ${{ env.WEBBOOT }} 30 | strategy: 31 | # Run all distros at the same time. 32 | max-parallel: 10 33 | # Continue testing other distros on failure. 34 | fail-fast: false 35 | # List of distros 36 | matrix: 37 | distro: 38 | - TinyCore 39 | - Arch 40 | - CentOS 7 41 | - Debian 42 | - Fedora 43 | - Kali 44 | - Linux Mint 45 | - Manjaro 46 | - Ubuntu 47 | # Steps represent a sequence of tasks that will be executed as part of the 48 | # job 49 | steps: 50 | - name: Install Go 51 | uses: actions/setup-go@v2 52 | with: 53 | go-version: 1.18.x 54 | - name: Checkout code 55 | uses: actions/checkout@v2 56 | with: 57 | path: ${{ env.WEBBOOT }} 58 | - name: Checkout dependencies 59 | run: | 60 | sudo apt-get update 61 | ./firsttime.sh 62 | - name: Integration testing 63 | run: | 64 | ./integration.sh "${{ matrix.distro }}" 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # webboot binary 2 | webboot 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Busybox directories. 15 | .bb/ 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | *~ 21 | 22 | # ignore kernels we build here, and firmware we download. 23 | linux 24 | linux-firmware 25 | 26 | # Ignore wpa_supplicant builds. 27 | wpa_supplicant-*.tar.gz 28 | wpa_supplicant-*.tar.gz.asc 29 | wpa_supplicant-*/ 30 | 31 | u-root 32 | 33 | # Integration testing artifacts 34 | integration/bzImage 35 | u-root 36 | 37 | # macOS 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to u-root 2 | 3 | We need help with this project. Contributions are very welcome. See the [roadmap](roadmap.md), open [issues](https://github.com/u-root/webboot/issues), and join us in [Slack](CONTRIBUTING.md#communication) to talk about your cool ideas for the project. 4 | 5 | ## Developer Sign-Off 6 | 7 | For purposes of tracking code-origination, we follow a simple sign-off 8 | process. If you can attest to the [Developer Certificate of 9 | Origin](https://developercertificate.org/) then you append in each git 10 | commit text a line such as: 11 | ``` 12 | Signed-off-by: Your Name 13 | ``` 14 | ## Code of Conduct 15 | 16 | Conduct collaboration in accordance to the [Code of 17 | Conduct](https://github.com/u-root/u-root/wiki/Code-of-Conduct). 18 | 19 | ## Communication 20 | 21 | - [Slack](https://u-root.slack.com), sign up 22 | [here](http://slack.u-root.com/) 23 | 24 | ## Bugs 25 | 26 | - Please submit issues to https://github.com/u-root/webboot/issues 27 | 28 | ## Coding Style 29 | 30 | The ``webboot`` project aims to follow the standard formatting recommendations 31 | and language idioms set out in the [Effective Go](https://golang.org/doc/effective_go.html) 32 | guide, for example [formatting](https://golang.org/doc/effective_go.html#formatting) 33 | and [names](https://golang.org/doc/effective_go.html#names). 34 | 35 | `gofmt` and `golint` are law, although this is not automatically enforced 36 | yet and some housecleaning needs done to achieve that. 37 | 38 | We have a few rules not covered by these tools: 39 | 40 | - Standard imports are separated from other imports. Example: 41 | ``` 42 | import ( 43 | "regexp" 44 | "time" 45 | 46 | dhcp "github.com/krolaw/dhcp4" 47 | ) 48 | ``` 49 | 50 | ## Patch Format 51 | 52 | Well formatted patches aide code review pre-merge and code archaeology in 53 | the future. The abstract form should be: 54 | ``` 55 | : Change summary 56 | 57 | More detailed explanation of your changes: Why and how. 58 | Wrap it to 72 characters. 59 | See [here] (http://chris.beams.io/posts/git-commit/) 60 | for some more good advices. 61 | 62 | Signed-off-by: 63 | ``` 64 | 65 | An example from this repo: 66 | ``` 67 | tcz: quiet it down 68 | 69 | It had a spurious print that was both annoying and making 70 | boot just a tad slower. 71 | 72 | Signed-off-by: Ronald G. Minnich 73 | ``` 74 | 75 | ## General Guidelines 76 | 77 | Webboot aims to be able to boot (optionally) signed distros from URLs. 78 | 79 | ## Pull Requests 80 | 81 | We accept GitHub pull requests. 82 | 83 | Fork the project on GitHub, work in your fork and in branches, push 84 | these to your GitHub fork, and when ready, do a GitHub pull requests 85 | against https://github.com/u-root/webboot. 86 | 87 | `webboot` uses go modules. Do not clone webboot into your GOPATH. 88 | If you add new dependencies, use the appropriate go modules command 89 | to update things. 90 | 91 | Every commit in your pull request needs to be able to build and pass the CI tests. 92 | 93 | If the pull request closes an issue please note it as: `"Fixes #NNN"`. 94 | 95 | ## Code Reviews 96 | 97 | Look at the area of code you're modifying, its history, and consider 98 | tagging some of the [maintainers](https://u-root.tk/#contributors) when doing a 99 | pull request in order to instigate some code review. 100 | 101 | ## Quality Controls 102 | 103 | We use Azure pipelines for our CI. 104 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:27a44a693abe815173ff862b763f95ccc228dd788fbfb71d7d15699048b49d74" 6 | name = "github.com/cenkalti/backoff" 7 | packages = ["."] 8 | pruneopts = "" 9 | revision = "18fe4ce5a8550e0d0919b680ad3c080a5455bddf" 10 | version = "v4.0.2" 11 | 12 | [[projects]] 13 | digest = "1:dec9486211fe71cc00a66bc9677b776b8926c3336c09509a16394e30e4b18b74" 14 | name = "github.com/gizak/termui" 15 | packages = [ 16 | "v3", 17 | "v3/drawille", 18 | "v3/widgets", 19 | ] 20 | pruneopts = "" 21 | revision = "4cca61d83fa2cc0f485c478ff768b0108f6591d6" 22 | version = "v3.1.0" 23 | 24 | [[projects]] 25 | digest = "1:d19d74ba2e4f95118d88c357fe6ad30840e28343e75023ffa96830d779974566" 26 | name = "github.com/insomniacslk/dhcp" 27 | packages = [ 28 | "dhcpv4", 29 | "dhcpv4/nclient4", 30 | "dhcpv6", 31 | "dhcpv6/nclient6", 32 | "iana", 33 | "interfaces", 34 | "rfc1035label", 35 | ] 36 | pruneopts = "" 37 | revision = "e1b69ee5fb3318ca17418526efa5bc41c98ff29b" 38 | 39 | [[projects]] 40 | digest = "1:59794624db141f0f0a893d110111b962984c9844cf565efa91c35bedb882c9ff" 41 | name = "github.com/mattn/go-runewidth" 42 | packages = ["."] 43 | pruneopts = "" 44 | revision = "14e809f6d78fcf9f48ff9b70981472b64c05f754" 45 | version = "v0.0.9" 46 | 47 | [[projects]] 48 | branch = "master" 49 | digest = "1:784945b75874aebb12fbba657773e9fce33b53c6832c7bd26cffd3253a9087c4" 50 | name = "github.com/mdlayher/ethernet" 51 | packages = ["."] 52 | pruneopts = "" 53 | revision = "0394541c37b7f86a10e0b49492f6d4f605c34163" 54 | 55 | [[projects]] 56 | branch = "master" 57 | digest = "1:ae8c7c747b36590d55929526d450231ebaacbce50937d065b58088e41ed413be" 58 | name = "github.com/mdlayher/raw" 59 | packages = ["."] 60 | pruneopts = "" 61 | revision = "50f2db8cc0658568575938a39dbaa46172921d98" 62 | 63 | [[projects]] 64 | digest = "1:713b341855f1480e4baca1e7c5434e1d266441340685ecbde32d59bdc065fb3f" 65 | name = "github.com/mitchellh/go-wordwrap" 66 | packages = ["."] 67 | pruneopts = "" 68 | revision = "9e67c67572bc5dd02aef930e2b0ae3c02a4b5a5c" 69 | version = "v1.0.0" 70 | 71 | [[projects]] 72 | branch = "master" 73 | digest = "1:1942df3305c51265563afcfea827a6b1207117acb7667d6870c03add7165bb1f" 74 | name = "github.com/nsf/termbox-go" 75 | packages = ["."] 76 | pruneopts = "" 77 | revision = "38ba6e5628f1d70bac606cfd210b9ad1a16c3027" 78 | 79 | [[projects]] 80 | branch = "master" 81 | digest = "1:c93c9eb031ecd6d4cee79cb880396811fea330f5170abb803f5be4bb417a80aa" 82 | name = "github.com/rekby/gpt" 83 | packages = ["."] 84 | pruneopts = "" 85 | revision = "7da10aec5566349f29875dad4a59c8341b01e00a" 86 | 87 | [[projects]] 88 | branch = "master" 89 | digest = "1:d7038d6b145dcc8ab8bd70301510cd12e85952daa8b68b96cbb851aa8e9b7f9e" 90 | name = "github.com/u-root/u-root" 91 | packages = [ 92 | "pkg/boot", 93 | "pkg/boot/ibft", 94 | "pkg/boot/kexec", 95 | "pkg/boot/multiboot", 96 | "pkg/boot/multiboot/internal/trampoline", 97 | "pkg/boot/syslinux", 98 | "pkg/cmdline", 99 | "pkg/curl", 100 | "pkg/dhclient", 101 | "pkg/mount", 102 | "pkg/mount/block", 103 | "pkg/mount/loop", 104 | "pkg/pci", 105 | "pkg/rand", 106 | "pkg/shlex", 107 | "pkg/ubinary", 108 | "pkg/uio", 109 | ] 110 | pruneopts = "" 111 | revision = "ad067b0a53cbe49d5b0c000ccb1acbe9ee0da08c" 112 | 113 | [[projects]] 114 | branch = "master" 115 | digest = "1:f923218863b1b913b1db4b1afc1a1204a203ccbf8b0bdb86bcd4a4da24f1f2ee" 116 | name = "github.com/vishvananda/netlink" 117 | packages = [ 118 | ".", 119 | "nl", 120 | ] 121 | pruneopts = "" 122 | revision = "98629f7ffc4b05a38794a4aa327ac810602a6d2e" 123 | 124 | [[projects]] 125 | branch = "master" 126 | digest = "1:096ba1cd91d38ee0bac7f4917c8f0d877b01c1fa7c61a8c4005e9eb79d140a86" 127 | name = "github.com/vishvananda/netns" 128 | packages = ["."] 129 | pruneopts = "" 130 | revision = "db3c7e526aae966c4ccfa6c8189b693d6ac5d202" 131 | 132 | [[projects]] 133 | branch = "master" 134 | digest = "1:92cf07fb2067987a7b46e1f63aa50ccde3ee8a7315ffca3e4e8a3a147ac9a717" 135 | name = "golang.org/x/net" 136 | packages = ["bpf"] 137 | pruneopts = "" 138 | revision = "ab34263943818b32f575efc978a3d24e80b04bd7" 139 | 140 | [[projects]] 141 | branch = "master" 142 | digest = "1:0ab3a6e867e19170427903297d997455ec237b4226fdd0778f25719b479cff7a" 143 | name = "golang.org/x/sys" 144 | packages = [ 145 | "internal/unsafeheader", 146 | "unix", 147 | ] 148 | pruneopts = "" 149 | revision = "5acd03effb828bdfdbad4e129e7daf84af6670b4" 150 | 151 | [[projects]] 152 | branch = "master" 153 | digest = "1:484f9b541e8ce421abeacddf297ea1f2db4461896854bc64fc6c1dc60b97821d" 154 | name = "pack.ag/tftp" 155 | packages = [ 156 | ".", 157 | "netascii", 158 | ] 159 | pruneopts = "" 160 | revision = "07909dfbde3c4e388a7e353351191fbb987ce5a5" 161 | 162 | [solve-meta] 163 | analyzer-name = "dep" 164 | analyzer-version = 1 165 | input-imports = [ 166 | "github.com/gizak/termui/v3", 167 | "github.com/gizak/termui/v3/widgets", 168 | "github.com/u-root/u-root/pkg/boot", 169 | "github.com/u-root/u-root/pkg/boot/kexec", 170 | "github.com/u-root/u-root/pkg/boot/syslinux", 171 | "github.com/u-root/u-root/pkg/dhclient", 172 | "github.com/u-root/u-root/pkg/mount", 173 | "github.com/u-root/u-root/pkg/mount/block", 174 | "github.com/u-root/u-root/pkg/mount/loop", 175 | "github.com/u-root/u-root/pkg/uio", 176 | "github.com/vishvananda/netlink", 177 | "golang.org/x/sys/unix", 178 | ] 179 | solver-name = "gps-cdcl" 180 | solver-version = 1 181 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/gizak/termui" 26 | version = "3.1.0" 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/u-root/u-root" 31 | 32 | [[constraint]] 33 | branch = "master" 34 | name = "github.com/vishvananda/netlink" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "golang.org/x/sys" 39 | 40 | [[override]] 41 | name = "github.com/insomniacslk/dhcp" 42 | revision = "e1b69ee5fb3318ca17418526efa5bc41c98ff29b" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2019, u-root Authors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | `webboot` offers tools to let a u-root instance boot signed live distro images over the web. 4 | 5 | ## Concept 6 | 7 | The `webboot` bootloader works as follows: 8 | 9 | 1. fetch an OS distro release ISO from the web 10 | 2. save the ISO to a local cache (ex. USB stick) 11 | 3. mount the ISO and copy out the kernel and initrd 12 | 4. load the extracted kernel with the initrd 13 | 5. kexec that kernel with parameters to tell the next distro where to locate its ISO file (ex. iso-scan/filename=) 14 | 15 | The current version offers a user interface based on [termui](https://github.com/gizak/termui) to help locate and boot the ISO file. 16 | 17 | For reference, webboot developers should familiarize themselves with: 18 | 19 | - [cpio tutorial](https://www.gnu.org/software/cpio/manual/html_node/Tutorial.html) 20 | - [initrd usage](https://www.kernel.org/doc/html/latest/admin-guide/initrd.html) 21 | - [kernel parameters](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html) 22 | 23 | 24 | ## Supported Operating Systems 25 | 26 | ### Requirements 27 | ISOs must have the following to be fully compatible with `webboot`. 28 | 29 | 1. 64-bit kernel 30 | 2. Parsable `grub` or `syslinux` config file 31 | 3. Init process than can locate an ISO file (ex. casper's iso-scan) 32 | 33 | Additional operating systems can be added by appending an entry to the `supportedDistros` map in `/cmds/webboot/types.go`. 34 | 35 | If the config file is not compatible with our parser, we can manually specify the configuration by adding a `Config` object to the distro's entry in `supportedDistros`. See the entries for Arch and Manjaro as an example. 36 | 37 | ### Currently Supported 38 | | Name | Required Kernel Parameters | Notes | 39 | | ----- | ------ | ----- | 40 | | Arch | `img_dev=/dev/disk/by-uuid/UUID img_loop=PATH_TO_ISO` | Unable to parse config file. Configuration is specified in a `Config` object. 41 | | CentOS | `iso-scan/filename=PATH_TO_ISO` | CentOS 7 supports live mode. CentOS 8 will boot to the graphical installer. 42 | | Debian | `findiso=PATH_TO_ISO` | 43 | | Fedora | `iso-scan/filename=PATH_TO_ISO` | 44 | | Kali | `findiso=PATH_TO_ISO` | 45 | | Linux Mint | `iso-scan/filename=PATH_TO_ISO` | 46 | | Manjaro | `img_dev=/dev/disk/by-uuid/UUID img_loop=PATH_TO_ISO` | Unable to parse config file. Configuration is specified in a `Config` object. 47 | | Tinycore | `iso=UUID/PATH_TO_ISO` | 48 | | Ubuntu | `iso-scan/filename=PATH_TO_ISO` | 49 | 50 | ### In Progress 51 | | Name | Required Kernel Parameters | Issue | 52 | | --- | --- | --- | 53 | | OpenSUSE | `root=live:CDLABEL=ISO_LABEL iso-scan/filename=PATH_TO_ISO` | `grub` config file is too complicated for our parser. We could specify the configuration manually, but that would involve hardcoding the ISO_LABEL (see [Issue 185](https://github.com/u-root/webboot/issues/185)).| 54 | 55 | ## Usage 56 | 57 | ### Build initramfs with added webboot commands 58 | 59 | Download u-root with `GO111MODULE=off go get github.com/u-root/u-root`. 60 | 61 | Run `GO111MODULE=off go run .` in the source directory of webboot to build the 62 | initramfs. 63 | 64 | This runs [u-root](https://github.com/u-root/u-root) under the hood. To pass 65 | extra options, such as to include extra files, use the `-u` switch, e.g., 66 | `GO111MODULE=off go run buildimage.go -u "-files path/to/bzImage:bzImage"` to 67 | add a custom kernel which can be used to test whether kexec works in a small 68 | setup. That saves a lot of time, because a full webboot flow would always need 69 | to download large ISO files, copy them, mount and decompress. 70 | 71 | #### Convenience 72 | 73 | For convenience, you can 74 | 75 | - skip the inclusion of Wi-Fi tools by passing `-wifi false` 76 | - add a custom kernel for within the initramfs via `-bzImage path/to/bzImage` 77 | - add an ISO file to the initramfs via `-iso path/to/os-distro.iso` 78 | * boot that ISO via `webboot -dhcp4=false -dhcp6-false local` later, which 79 | requires passing a pmem-enabled kernel via `-bzImage` as described above 80 | 81 | #### Compression 82 | 83 | You can optionally compress the initramfs with `lzma` or any other compression 84 | method you configure your kernel for. 85 | 86 | ```sh 87 | lzma -f /tmp/initramfs.linux_amd64.cpio 88 | ``` 89 | 90 | Refer to [u-root's documentation](https://github.com/u-root/u-root#compression) 91 | for more details on compression. 92 | 93 | #### Customization 94 | 95 | The `buildimage.go` utility is really just a helper tool. Instead of using it, 96 | you can build a custom u-root image as you like and add the `webboot` binary to 97 | it. 98 | Refer to [u-root's usage documentation](https://github.com/u-root/u-root#usage) 99 | for details. 100 | 101 | ### Building a kernel for webboot 102 | 103 | webboot uses a standard Linux kernel which should be fairly portable, based on a 104 | Long Term Stable (LTS) release. It has worked on every Chromebook we tried. 105 | 106 | This kernel is built using a config originally from 107 | [NiChromeOS](github.com/NiChrome/NiChrome). 108 | If we are building a bootable USB stick formatted with vfat, we don't have the 109 | space constraints of NiChrome, so we expect this to diverge over time. 110 | 111 | Nevertheless, to keep it all simple, we build it as a non-modular kernel with 112 | Wi-fi firmware built-in. We no longer build the initramfs into the kernel, as 113 | that's not needed. 114 | 115 | Make sure the kernel configuration includes the firmware for your network device. 116 | For instance, the Thinkpad x240 with Intel Corporation Wireless 7260 uses 117 | iwlwifi-7260-17.ucode. If you look at the kernel config file, this firmware name 118 | is included under `CONFIG_EXTRA_FIRMWARE=`. 119 | 120 | To build, first be sure you're in a directory you want to be in! 121 | You can actually do the work in the webboot root directory because the 122 | `.gitignore` file ignores the two directories you create when following the 123 | instructions here. 124 | 125 | #### Prerequisites 126 | 127 | You need to have the following packages installed if on Ubuntu: 128 | ```sh 129 | sudo apt install libssl-dev build-essential wireless-tools kexec-tools libelf-dev libnl-3-dev libnl-genl-3-dev 130 | ``` 131 | 132 | #### Fetching, configuring and compiling the kernel 133 | 134 | ```sh 135 | git clone --depth 1 -b v5.6.14 \ 136 | git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git linux 137 | git clone \ 138 | git://git.kernel.org/pub/scm/linux/kernel/git/iwlwifi/linux-firmware.git 139 | cp config-5.6.14 linux/.config 140 | (cd linux && make bzImage) 141 | GO111MODULE=off go run . 142 | ``` 143 | 144 | ### Testing in QEMU 145 | 146 | Install QEMU with 147 | ```sh 148 | sudo apt-get install qemu-system-x86 149 | ``` 150 | 151 | Run the following, and a QEMU window should pop up: 152 | 153 | Tip: Don't use the `-nographic` option for u-root in QEMU as you want to boot 154 | into a graphical interface. 155 | 156 | #### Acceleration 157 | 158 | If you have KVM in your host system, you can add `-enable-kvm` for speedup. 159 | 160 | ```sh 161 | qemu-system-x86_64 \ 162 | -enable-kvm \ 163 | -m 2G \ 164 | -kernel linux/arch/x86/boot/bzImage \ 165 | -append 'console=ttyS0 console=tty1 memmap=1G!1G' \ 166 | -initrd /tmp/initramfs.linux_amd64.cpio \ 167 | -device virtio-rng-pci \ 168 | -netdev user,id=network0 \ 169 | -device rtl8139,netdev=network0 170 | ``` 171 | 172 | Tip: Don't use the `-nographic` option for u-root in QEMU as you want to boot 173 | into a graphical interface. 174 | 175 | In the QEMU terminal, run 176 | ```sh 177 | dhclient -ipv6=f 178 | ``` 179 | and then run 180 | ```sh 181 | webboot 182 | ``` 183 | 184 | Refer to 185 | [u-root's documentation](https://github.com/u-root/u-root#testing-in-qemu) for 186 | more details on virtualization. 187 | 188 | ### Testing with a USB stick 189 | 190 | You can try out webboot from a USB stick. That means that you could run it when 191 | starting a machine by choosing to boot from USB, which requires a bootloader. 192 | Although any bootloader would do, we will focus on one here named `syslinux`. 193 | Furthermore, we will focus on specific preconditions, although there are many 194 | different ways to create a bootable USB stick. 195 | 196 | In the root directory of this repository, there is an example configuration file 197 | named `syslinux.cfg.example`. If you look at it, you will see that it resembles 198 | webboot very much: It lists a kernel, an initrd, and extra arguments to append. 199 | 200 | Before you continue, please make sure to meet the following conditions: 201 | 202 | - your system can boot from MBR (possibly through UEFI CSM) 203 | - You have a directory `/mnt/usb` to mount the partition to 204 | 205 | To [install](https://wiki.syslinux.org/wiki/index.php?title=Install) syslinux as 206 | a bootloader and configure it, four steps are necessary: 207 | 208 | 1. Write a Volume Boot Record (VBR) to the stick 209 | 2. Write a Master Boot Record (MBR) to it 210 | 3. Mark the first partition as bootable 211 | 4. Copy the config file, Linux kernel, and initcpio 212 | 213 | The following instructions will walk you through these four steps. 214 | Tip: You may need to replace `sdb1` with the name of your partition. 215 | 216 | Install syslinux with 217 | ```sh 218 | sudo apt-get install syslinux 219 | ``` 220 | 221 | To prepare your USB stick, run `sudo fdisk /dev/sdb` and use the fdisk instructions to complete the following: 222 | 1. Delete all existing partitions (d) 223 | 2. Add one new partition (n, p, 1) 224 | 3. Change partition type (t) to EFI (ef) 225 | 4. Make partition 1 bootable (a) 226 | 5. Save (w) 227 | 228 | Here is a sample fdisk output: 229 | ```sh 230 | $ sudo fdisk /dev/sdb 231 | 232 | Welcome to fdisk (util-linux 2.36.1). 233 | Changes will remain in memory only, until you decide to write them. 234 | Be careful before using the write command. 235 | 236 | 237 | Command (m for help): d 238 | Selected partition 1 239 | Partition 1 has been deleted. 240 | 241 | Command (m for help): n 242 | Partition type 243 | p primary (0 primary, 0 extended, 4 free) 244 | e extended (container for logical partitions) 245 | Select (default p): p 246 | Partition number (1-4, default 1): 1 247 | First sector (2048-121061375, default 2048): 248 | Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-121061375, default 121061375): 249 | 250 | Created a new partition 1 of type 'Linux' and of size 57.7 GiB. 251 | 252 | Command (m for help): t 253 | Selected partition 1 254 | Hex code or alias (type L to list all): L 255 | 256 | 00 Empty 24 NEC DOS 81 Minix / old Lin bf Solaris 257 | 01 FAT12 27 Hidden NTFS Win 82 Linux swap / So c1 DRDOS/sec (FAT- 258 | 02 XENIX root 39 Plan 9 83 Linux c4 DRDOS/sec (FAT- 259 | 03 XENIX usr 3c PartitionMagic 84 OS/2 hidden or c6 DRDOS/sec (FAT- 260 | 04 FAT16 <32M 40 Venix 80286 85 Linux extended c7 Syrinx 261 | 05 Extended 41 PPC PReP Boot 86 NTFS volume set da Non-FS data 262 | 06 FAT16 42 SFS 87 NTFS volume set db CP/M / CTOS / . 263 | 07 HPFS/NTFS/exFAT 4d QNX4.x 88 Linux plaintext de Dell Utility 264 | 08 AIX 4e QNX4.x 2nd part 8e Linux LVM df BootIt 265 | 09 AIX bootable 4f QNX4.x 3rd part 93 Amoeba e1 DOS access 266 | 0a OS/2 Boot Manag 50 OnTrack DM 94 Amoeba BBT e3 DOS R/O 267 | 0b W95 FAT32 51 OnTrack DM6 Aux 9f BSD/OS e4 SpeedStor 268 | 0c W95 FAT32 (LBA) 52 CP/M a0 IBM Thinkpad hi ea Linux extended 269 | 0e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a5 FreeBSD eb BeOS fs 270 | 0f W95 Ext'd (LBA) 54 OnTrackDM6 a6 OpenBSD ee GPT 271 | 10 OPUS 55 EZ-Drive a7 NeXTSTEP ef EFI (FAT-12/16/ 272 | 11 Hidden FAT12 56 Golden Bow a8 Darwin UFS f0 Linux/PA-RISC b 273 | 12 Compaq diagnost 5c Priam Edisk a9 NetBSD f1 SpeedStor 274 | 14 Hidden FAT16 <3 61 SpeedStor ab Darwin boot f4 SpeedStor 275 | 16 Hidden FAT16 63 GNU HURD or Sys af HFS / HFS+ f2 DOS secondary 276 | 17 Hidden HPFS/NTF 64 Novell Netware b7 BSDI fs fb VMware VMFS 277 | 18 AST SmartSleep 65 Novell Netware b8 BSDI swap fc VMware VMKCORE 278 | 1b Hidden W95 FAT3 70 DiskSecure Mult bb Boot Wizard hid fd Linux raid auto 279 | 1c Hidden W95 FAT3 75 PC/IX bc Acronis FAT32 L fe LANstep 280 | 1e Hidden W95 FAT1 80 Old Minix be Solaris boot ff BBT 281 | 282 | Aliases: 283 | linux - 83 284 | swap - 82 285 | extended - 05 286 | uefi - EF 287 | raid - FD 288 | lvm - 8E 289 | linuxex - 85 290 | Hex code or alias (type L to list all): EF 291 | Changed type of partition 'Linux' to 'EFI (FAT-12/16/32)'. 292 | 293 | Command (m for help): a 294 | Selected partition 1 295 | The bootable flag on partition 1 is enabled now. 296 | 297 | Command (m for help): w 298 | The partition table has been altered. 299 | Calling ioctl() to re-read partition table. 300 | Syncing disks. 301 | ``` 302 | 303 | Generate the partition header 304 | ```sh 305 | mkfs -t vfat /dev/sdb1 306 | ``` 307 | 308 | Mount the USB and copy the config file, Linux kernel, and initcpio 309 | ```sh 310 | sudo mount /dev/sdb1 /mnt/usb 311 | cp config-5.6.14 /mnt/usb/ 312 | cp arch/x86/boot/bzImage /mnt/usb 313 | cp /tmp/initramfs.linux_amd64.cpio /mnt/usb 314 | umount /mnt/usb 315 | ``` 316 | 317 | Zip initramfs 318 | ```sh 319 | gzip /tmp/initramfs.linux_amd64.cpio 320 | ``` 321 | 322 | Now the following commands would need to be run as root: 323 | 324 | ```sh 325 | syslinux -i /dev/sdb1 326 | dd bs=440 count=1 conv=notrunc if=/usr/lib/syslinux/mbr/mbr.bin of=/dev/sdb 327 | parted /dev/sdb set 1 boot on 328 | # mount the stick and copy the files 329 | mount /dev/sdb1 /mnt/usb 330 | cp syslinux.cfg.example /mnt/usb/syslinux.cfg 331 | mkdir /mnt/usb/boot 332 | cp linux/arch/x86/boot/bzImage /mnt/usb/boot/webboot 333 | cp /tmp/initramfs.linux_amd64.cpio.gz /mnt/usb/boot/webboot.cpio.gz 334 | ``` 335 | 336 | Finally, we need to create a `/Images` directory at the root of the usb stick. Note that the "I" in "Images" needs to be capitalized. 337 | 338 | ```sh 339 | mkdir /mnt/usb/Images 340 | ``` 341 | 342 | You should be able to boot from the USB stick now. Depending on your firmware 343 | setup, it might be necessary to get into a boot menu or make changes in the 344 | settings. 345 | 346 | To rebuild the USB stick, you can run 347 | ```sh 348 | sh makeusb.sh sdb1 349 | ``` 350 | -------------------------------------------------------------------------------- /RR: -------------------------------------------------------------------------------- 1 | RAM_SIZE=${RAM_SIZE:=4G} 2 | KERNEL=${KERNEL:=linux/arch/x86/boot/bzImage} 3 | INITRD=${INITRD:=/tmp/initramfs.linux_amd64.cpio} 4 | 5 | /usr/bin/qemu-system-x86_64 \ 6 | -machine q35 \ 7 | -m $RAM_SIZE \ 8 | -object rng-random,filename=/dev/urandom,id=rng0 \ 9 | -device virtio-rng-pci,rng=rng0 \ 10 | -netdev user,id=u1 -device rtl8139,netdev=u1 \ 11 | -kernel $KERNEL \ 12 | -initrd $INITRD \ 13 | -vga std \ 14 | -append "earlyprintk=ttyS0,115200,keep console=tty0 vga=ask" \ 15 | -serial stdio 16 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Check that the code has been formatted correctly. 5 | GOFMT_DIFF=$(gofmt -s -d *.go pkg cmds) 6 | if [[ -n "${GOFMT_DIFF}" ]]; then 7 | echo 'Error: Go source code is not formatted:' 8 | printf '%s\n' "${GOFMT_DIFF}" 9 | echo 'Run `gofmt -s -w *.go pkg cmds' 10 | exit 1 11 | fi 12 | 13 | go mod tidy 14 | go build . 15 | ./webboot 16 | if [ ! -f "/tmp/initramfs.linux_amd64.cpio" ]; then 17 | echo "Initrd was not created." 18 | exit 1 19 | fi 20 | 21 | (cd cmds/webboot && go test -v) 22 | (cd pkg/menu && go test -v) 23 | (cd pkg/bootiso && sudo -E env "PATH=$PATH" go test -v) # need sudo to mount the test iso 24 | -------------------------------------------------------------------------------- /cmds/cli/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "Arch": { 3 | "isoPattern": "^archlinux-.+", 4 | "checksum": "41c5d5c181faebcff9a6cdd9e270d87dd9d766507687e4555c7852d198d0ad48", 5 | "checksumType": "sha256", 6 | "kernelParams": "img_dev=/dev/disk/by-uuid/{{.UUID}} img_loop={{.IsoPath}}", 7 | "customConfigs": [ 8 | { 9 | "Label": "Default Config", 10 | "KernelPath": "/arch/boot/x86_64/vmlinuz-linux", 11 | "InitrdPath": "/arch/boot/x86_64/archiso.img", 12 | "Cmdline": "" 13 | } 14 | ], 15 | "mirrors": [ 16 | { 17 | "name": "Default", 18 | "url": "https://mirrors.acm.wpi.edu/archlinux/iso/2022.09.03/archlinux-2022.09.03-x86_64.iso" 19 | } 20 | ] 21 | }, 22 | "CentOS 7": { 23 | "isoPattern": "^CentOS-7.+", 24 | "checksum": "689531cce9cf484378481ae762fae362791a9be078fda10e4f6977bf8fa71350", 25 | "checksumType": "sha256", 26 | "bootConfig": "grub", 27 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 28 | "mirrors": [ 29 | { 30 | "name": "Default", 31 | "url": "https://mirrors.ocf.berkeley.edu/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Everything-2009.iso" 32 | } 33 | ] 34 | }, 35 | "Debian": { 36 | "isoPattern": "^debian-.+", 37 | "checksum": "99a532675ec9733c277a3f4661638b5471dc5bce989b3a2dbc3ac694c964a7f7", 38 | "checksumType": "sha256", 39 | "bootConfig": "syslinux", 40 | "kernelParams": "findiso={{.IsoPath}}", 41 | "mirrors": [ 42 | { 43 | "name": "Default", 44 | "url": "https://cdimage.debian.org/debian-cd/11.5.0/amd64/iso-dvd/debian-11.5.0-amd64-DVD-1.iso" 45 | } 46 | ] 47 | }, 48 | "Fedora": { 49 | "isoPattern": "^Fedora-.+", 50 | "checksum": "80169891cb10c679cdc31dc035dab9aae3e874395adc5229f0fe5cfcc111cc8c", 51 | "checksumType": "sha256", 52 | "bootConfig": "grub", 53 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 54 | "mirrors": [ 55 | { 56 | "name": "Default", 57 | "url": "https://download.fedoraproject.org/pub/fedora/linux/releases/36/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-36-1.5.iso" 58 | } 59 | ] 60 | }, 61 | "Kali": { 62 | "isoPattern": "^kali-linux-.+", 63 | "checksum": "f87618a6df20b6fdf4edebee1c6f1d808dee075a431229b3f75a5208e3c9c0e8", 64 | "checksumType": "sha256", 65 | "bootConfig": "grub", 66 | "kernelParams": "findiso={{.IsoPath}}", 67 | "mirrors": [ 68 | { 69 | "name": "Default", 70 | "url": "https://cdimage.kali.org/kali-2022.3/kali-linux-2022.3-live-amd64.iso" 71 | } 72 | ] 73 | }, 74 | "Linux Mint": { 75 | "isoPattern": "^linuxmint-.+", 76 | "checksum": "f524114e4a10fb04ec428af5e8faf7998b18271ea72fbb4b63efe0338957c0f3", 77 | "checksumType": "sha256", 78 | "bootConfig": "grub", 79 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 80 | "mirrors": [ 81 | { 82 | "name": "Default", 83 | "url": "https://mirrors.edge.kernel.org/linuxmint/stable/21/linuxmint-21-cinnamon-64bit.iso" 84 | } 85 | ] 86 | }, 87 | "Manjaro": { 88 | "isoPattern": "^manjaro-.+", 89 | "checksum": "63b76319e4ca91d626e2bd30d34e841e134baec9", 90 | "checksumType": "sha1", 91 | "kernelParams": "img_dev=/dev/disk/by-uuid/{{.UUID}} img_loop={{.IsoPath}}", 92 | "customConfigs": [ 93 | { 94 | "Label": "Default Config", 95 | "KernelPath": "/boot/vmlinuz-x86_64", 96 | "InitrdPath": "/boot/initramfs-x86_64.img", 97 | "Cmdline": "driver=free tz=utc lang=en_US keytable=en" 98 | } 99 | ], 100 | "mirrors": [ 101 | { 102 | "name": "Default", 103 | "url": "https://download.manjaro.org/xfce/21.3.7/manjaro-xfce-21.3.7-220816-linux515.iso" 104 | } 105 | ] 106 | }, 107 | "TinyCore": { 108 | "isoPattern": ".*CorePure64-.+", 109 | "checksum": "84b488347246ac9ded4c4a09c3800306", 110 | "checksumType": "md5", 111 | "bootConfig": "syslinux", 112 | "kernelParams": "iso=UUID={{.UUID}}{{.IsoPath}} console=ttyS0 earlyprintk=ttyS0", 113 | "mirrors": [ 114 | { 115 | "name": "Default", 116 | "url": "http://tinycorelinux.net/13.x/x86_64/release/TinyCorePure64-13.1.iso" 117 | } 118 | ] 119 | }, 120 | "Ubuntu": { 121 | "isoPattern": "^ubuntu-.+", 122 | "checksum": "c396e956a9f52c418397867d1ea5c0cf1a99a49dcf648b086d2fb762330cc88d", 123 | "checksumType": "sha256", 124 | "bootConfig": "syslinux", 125 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 126 | "mirrors": [ 127 | { 128 | "name": "Default", 129 | "url": "https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso" 130 | } 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /cmds/cli/main.go: -------------------------------------------------------------------------------- 1 | // Package main downloads and boots an ISO. 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "html/template" 11 | "io/ioutil" 12 | "log" 13 | "path" 14 | "path/filepath" 15 | 16 | "github.com/u-root/u-root/pkg/boot" 17 | "github.com/u-root/webboot/pkg/bootiso" 18 | "github.com/u-root/webboot/pkg/menu" 19 | ) 20 | 21 | var ( 22 | v = flag.Bool("verbose", false, "Verbose output") 23 | verbose = func(string, ...interface{}) {} 24 | dir = flag.String("dir", "", "Path of cached directory") 25 | dryRun = flag.Bool("dryrun", false, "If dry_run is true we won't boot the iso.") 26 | distroName = flag.String("distroName", "", "This is the distro that will be tested.") 27 | cacheDev CacheDevice 28 | logBuffer bytes.Buffer 29 | tmpBuffer bytes.Buffer 30 | ) 31 | 32 | // ISO's exec downloads the iso and boot it. 33 | func (i *ISO) exec(enableBoot bool) error { 34 | verbose("Intent to boot %s", i.path) 35 | 36 | distro, ok := supportedDistros[*distroName] 37 | if !ok { 38 | return fmt.Errorf("Could not infer ISO type based on filename.") 39 | } 40 | 41 | verbose("Using distro %s with boot config %s", *distroName, distro.BootConfig) 42 | 43 | var configs []boot.OSImage 44 | if distro.BootConfig != "" { 45 | parsedConfigs, err := bootiso.ParseConfigFromISO(i.path, distro.BootConfig) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | configs = append(configs, parsedConfigs...) 51 | } 52 | 53 | if len(distro.CustomConfigs) != 0 { 54 | customConfigs, err := bootiso.LoadCustomConfigs(i.path, distro.CustomConfigs) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | configs = append(configs, customConfigs...) 60 | } 61 | 62 | if len(configs) == 0 { 63 | return fmt.Errorf("No valid configs were found.") 64 | } 65 | 66 | verbose("Get configs: %+v", configs) 67 | 68 | entries := []menu.Entry{} 69 | for _, config := range configs { 70 | entries = append(entries, &BootConfig{config}) 71 | } 72 | 73 | config, ok := entries[0].(*BootConfig) 74 | if !ok { 75 | return fmt.Errorf("Could not convert selection to a boot image.") 76 | } 77 | 78 | paramTemplate, err := template.New("template").Parse(distro.KernelParams) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | var kernelParams bytes.Buffer 84 | if err = paramTemplate.Execute(&kernelParams, cacheDev); err != nil { 85 | return err 86 | } 87 | 88 | if !enableBoot { 89 | s := fmt.Sprintf("config.image %s, kernelparams.String() %s", config.image, kernelParams.String()) 90 | return fmt.Errorf("Booting is disabled (see --dryrun flag), but otherwise would be [%s].", s) 91 | } 92 | err = bootiso.BootCachedISO(config.image, kernelParams.String()+" waitusb=10") 93 | 94 | // If kexec succeeds, we should not arrive here 95 | if err == nil { 96 | // TODO: We should know whether we tried using /sbin/kexec. 97 | err = fmt.Errorf("kexec failed, but gave no error. Consider trying kexec-tools.") 98 | } 99 | 100 | return err 101 | } 102 | 103 | // DownloadOption's exec lets user input the name of the iso they want 104 | // if this iso is existed in the bookmark, use it's url 105 | // elsewise ask for a download link 106 | func (d *DownloadOption) exec() (menu.Entry, error) { 107 | 108 | link := supportedDistros[*distroName].Mirrors[0].Url 109 | 110 | filename := path.Base(link) 111 | 112 | // If the cachedir is not find, downloaded the iso to /tmp, else create a Downloaded dir in the cache dir. 113 | var fpath string 114 | var downloadDir string 115 | var err error 116 | 117 | // "/testdata" directly accesses the host filesystem (which is presumably on a 118 | // hard drive). Because initramfs is mounted on RAM, which has limited space, 119 | // downloading an ISO to the hard drive is often necessary. Note that this is 120 | // a hacky workaround; ideally, when testing, initramfs would be mounted on 121 | // the hard drive instead of RAM so there's enough space in `os.TempDir()` for 122 | // an entire ISO. 123 | downloadDir = "/testdata" 124 | fpath = filepath.Join(downloadDir, filename) 125 | 126 | if err = download(link, fpath, downloadDir); err != nil { 127 | if err == context.Canceled { 128 | return nil, fmt.Errorf("Download was canceled.") 129 | } else { 130 | return nil, err 131 | } 132 | } 133 | 134 | return &ISO{label: filename, path: fpath}, nil 135 | } 136 | 137 | // distroData downloads and parses the data in distros.json to a map[string]Distro. 138 | func distroData() error { 139 | jsonPath := "/ci.json" 140 | 141 | // Parse the json file. 142 | data, err := ioutil.ReadFile(jsonPath) 143 | if err != nil { 144 | return fmt.Errorf("Could not read JSON file: %v\n", err) 145 | } 146 | 147 | err = json.Unmarshal([]byte(data), &supportedDistros) 148 | if err != nil { 149 | return fmt.Errorf("Could not unmarshal JSON file: %v\n", err) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func main() { 156 | flag.Parse() 157 | if flag.NArg() != 0 { 158 | fmt.Errorf("Unexpected positional arguments: %v", flag.Args()) 159 | } 160 | if *v { 161 | verbose = log.Printf 162 | } 163 | 164 | var err error 165 | var entry menu.Entry 166 | 167 | // get distro data 168 | err = distroData() 169 | if err != nil { 170 | log.Fatalf("Error on distroData(): %v", err.Error()) 171 | } 172 | 173 | // DownloadOption 174 | entry = &DownloadOption{} 175 | entry, err = entry.(*DownloadOption).exec() 176 | if err != nil { 177 | log.Fatalf("Error on (*DownloadOption).exec(): %v", err.Error()) 178 | } 179 | 180 | // ISO 181 | if err = entry.(*ISO).exec(!*dryRun); err != nil { 182 | log.Fatalf("Error on (*ISO).exec(): %v", err.Error()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /cmds/cli/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/u-root/u-root/pkg/boot" 5 | "github.com/u-root/u-root/pkg/mount/block" 6 | "github.com/u-root/webboot/pkg/bootiso" 7 | "github.com/u-root/webboot/pkg/menu" 8 | ) 9 | 10 | type Distro struct { 11 | IsoPattern string 12 | Checksum string 13 | ChecksumType string 14 | BootConfig string 15 | KernelParams string 16 | CustomConfigs []bootiso.Config 17 | Mirrors []Mirror 18 | } 19 | 20 | type Mirror struct { 21 | Name string 22 | Url string 23 | } 24 | 25 | func (m *Mirror) Label() string { 26 | return m.Name 27 | } 28 | 29 | var supportedDistros = map[string]Distro{} 30 | 31 | type CacheDevice struct { 32 | Name string 33 | UUID string 34 | MountPoint string 35 | IsoPath string // set after iso is selected 36 | } 37 | 38 | func NewCacheDevice(device *block.BlockDev, mountPoint string) CacheDevice { 39 | return CacheDevice{ 40 | Name: device.Name, 41 | UUID: device.FsUUID, 42 | MountPoint: mountPoint, 43 | } 44 | } 45 | 46 | // ISO contains information of the iso user wants to boot. 47 | type ISO struct { 48 | label string 49 | path string 50 | checksum string 51 | } 52 | 53 | var _ = menu.Entry(&ISO{}) 54 | 55 | // Label is the string this iso displays in the menu page. 56 | func (i *ISO) Label() string { 57 | return i.label 58 | } 59 | 60 | // Config represents one kind of configure of booting an iso. 61 | type Config struct { 62 | label string 63 | } 64 | 65 | var _ = menu.Entry(&Config{}) 66 | 67 | // Label is the string this iso displays in the menu page. 68 | func (c *Config) Label() string { 69 | return c.label 70 | } 71 | 72 | // DownloadOption lets the user download an iso then boot it. 73 | type DownloadOption struct { 74 | } 75 | 76 | var _ = menu.Entry(&DownloadOption{}) 77 | 78 | // Label is the string this iso displays in the menu page. 79 | func (d *DownloadOption) Label() string { 80 | return "Download an ISO" 81 | } 82 | 83 | // DirOption represents a directory under cache directory. 84 | // It displays its sub-directory or iso files. 85 | type DirOption struct { 86 | label string 87 | path string 88 | } 89 | 90 | var _ = menu.Entry(&DirOption{}) 91 | 92 | // Label is the string this option displays in the menu page. 93 | func (d *DirOption) Label() string { 94 | return d.label 95 | } 96 | 97 | type Interface struct { 98 | label string 99 | } 100 | 101 | func (i *Interface) Label() string { 102 | return i.label 103 | } 104 | 105 | type BootConfig struct { 106 | image boot.OSImage 107 | } 108 | 109 | func (b *BootConfig) Label() string { 110 | return b.image.Label() 111 | } 112 | -------------------------------------------------------------------------------- /cmds/cli/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "math" 9 | "net/http" 10 | "os" 11 | "sort" 12 | 13 | "github.com/u-root/webboot/pkg/menu" 14 | ) 15 | 16 | // WriteCounter counts the number of bytes written to it. It implements an io.Writer 17 | type WriteCounter struct { 18 | received float64 19 | expected float64 20 | progress menu.Progress 21 | } 22 | 23 | func NewWriteCounter(expectedSize int64) WriteCounter { 24 | return WriteCounter{0, float64(expectedSize), menu.NewProgress("", false)} 25 | } 26 | 27 | // QEMU testing uses serial output, so termui cannot be used. Instead, 28 | // download percentage is logged when it's roughly a whole number 29 | func (wc *WriteCounter) Write(p []byte) (int, error) { 30 | n := len(p) 31 | wc.received += float64(n) 32 | percentage := 100 * (wc.received / wc.expected) 33 | 34 | // `percentage` is logged when "close enough" to a whole number, which depends 35 | // on how big the written chunk is (to account for different download 36 | // conditions causing chunks to vary in size) 37 | threshold := float64(n) / wc.expected * 100 38 | const megabyte = 1_000_000 39 | const gigabyte = 1_000_000_000 40 | if math.Abs(percentage-math.Trunc(percentage)) < threshold { 41 | verbose("Downloading... %.2f%% (%.3f MB / %.3f GB)", 42 | 100*(wc.received/wc.expected), 43 | wc.received/megabyte, 44 | wc.expected/gigabyte) 45 | } 46 | 47 | return n, nil 48 | } 49 | 50 | func (wc *WriteCounter) Close() { 51 | wc.progress.Close() 52 | } 53 | 54 | // download() will download a file from URL and save it to a temp file 55 | // If the download succeeds, the temp file will be copied to fPath 56 | func download(URL, fPath, downloadDir string) error { 57 | ctx, cancel := context.WithCancel(context.Background()) 58 | defer cancel() 59 | 60 | req, err := http.NewRequestWithContext(ctx, "GET", URL, nil) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | resp, err := http.DefaultClient.Do(req) 66 | if err != nil { 67 | return err 68 | } 69 | if resp.StatusCode != 200 { 70 | return fmt.Errorf("Received http status code %s", resp.Status) 71 | } 72 | defer resp.Body.Close() 73 | 74 | tempFile, err := ioutil.TempFile(downloadDir, "iso-download-") 75 | if err != nil { 76 | return err 77 | } 78 | defer os.Remove(tempFile.Name()) 79 | 80 | counter := NewWriteCounter(resp.ContentLength) 81 | 82 | if _, err = io.Copy(tempFile, io.TeeReader(resp.Body, &counter)); err != nil { 83 | counter.Close() 84 | return err 85 | } 86 | 87 | counter.Close() 88 | 89 | if _, err = tempFile.Seek(0, 0); err != nil { 90 | return err 91 | } 92 | 93 | err = os.Rename(tempFile.Name(), fPath) 94 | if err != nil { 95 | return fmt.Errorf("Error on os.Rename: %v", err) 96 | } 97 | 98 | verbose("%q is downloaded at %q\n", URL, fPath) 99 | return nil 100 | } 101 | 102 | func supportedDistroEntries() []menu.Entry { 103 | entries := []menu.Entry{} 104 | for distroName := range supportedDistros { 105 | entries = append(entries, &Config{label: distroName}) 106 | } 107 | 108 | sort.Slice(entries[:], func(i, j int) bool { 109 | return entries[i].Label() < entries[j].Label() 110 | }) 111 | 112 | return entries 113 | } 114 | -------------------------------------------------------------------------------- /cmds/webboot/Readme.md: -------------------------------------------------------------------------------- 1 | ### Usage 2 | The webboot program would do the following: 3 | - Present a menu with the existing cached distro options 4 | - If the user wants a distro that is not cached, they can download an ISO 5 | - After the user decides on an ISO, boot it. 6 | 7 | ### Test 8 | Our UI uses a package called Termui. Termui will parse the standard input into keyboard events and insert them into a channel, then from which the Termui get it's input. For implement a unattended test, I manually build a series of keyboard events that reperesent my intented input for test, and insert them into a channel. Then I replace the original input channel with my channel in the test. So the go test could run a test of ui automatically. 9 | 10 | See TestDownloadOption for an example: 11 | - create a channel by make(chan ui.Event). 12 | - use go pressKey(uiEvents, input) to translate the intented test input to keyboard events and push them to the uiEvents chanel. 13 | - use the uiEvents channel by call downloadOption.exec(uiEvents). (Main function will always call ui.PollEvents() to get the sandard input channel) 14 | - all functions involving in ui input will provide a argument to indicate the input chanel. 15 | 16 | ### Hint 17 | If want to set up a cached directory in side the USB stick, the file structure of USB stick should be 18 | +-- USB root 19 | | +-- Images (<--- the cache directory. It must be named as "Images") 20 | | +-- subdirectories or iso files 21 | ... -------------------------------------------------------------------------------- /cmds/webboot/distros.json: -------------------------------------------------------------------------------- 1 | { 2 | "Arch": { 3 | "isoPattern": "^archlinux-.+", 4 | "checksum": "41c5d5c181faebcff9a6cdd9e270d87dd9d766507687e4555c7852d198d0ad48", 5 | "checksumType": "sha256", 6 | "kernelParams": "img_dev=/dev/disk/by-uuid/{{.UUID}} img_loop={{.IsoPath}}", 7 | "customConfigs": [ 8 | { 9 | "Label": "Default Config", 10 | "KernelPath": "/arch/boot/x86_64/vmlinuz-linux", 11 | "InitrdPath": "/arch/boot/x86_64/archiso.img", 12 | "Cmdline": "" 13 | } 14 | ], 15 | "mirrors": [ 16 | { 17 | "name": "Default", 18 | "url": "https://mirrors.acm.wpi.edu/archlinux/iso/2022.09.03/archlinux-2022.09.03-x86_64.iso" 19 | } 20 | ] 21 | }, 22 | "CentOS 7": { 23 | "isoPattern": "^CentOS-7.+", 24 | "checksum": "689531cce9cf484378481ae762fae362791a9be078fda10e4f6977bf8fa71350", 25 | "checksumType": "sha256", 26 | "bootConfig": "grub", 27 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 28 | "mirrors": [ 29 | { 30 | "name": "Default", 31 | "url": "https://mirrors.ocf.berkeley.edu/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-Everything-2009.iso" 32 | } 33 | ] 34 | }, 35 | "Debian": { 36 | "isoPattern": "^debian-.+", 37 | "checksum": "99a532675ec9733c277a3f4661638b5471dc5bce989b3a2dbc3ac694c964a7f7", 38 | "checksumType": "sha256", 39 | "bootConfig": "syslinux", 40 | "kernelParams": "findiso={{.IsoPath}}", 41 | "mirrors": [ 42 | { 43 | "name": "Default", 44 | "url": "https://cdimage.debian.org/debian-cd/11.5.0/amd64/iso-dvd/debian-11.5.0-amd64-DVD-1.iso" 45 | } 46 | ] 47 | }, 48 | "Fedora": { 49 | "isoPattern": "^Fedora-.+", 50 | "checksum": "80169891cb10c679cdc31dc035dab9aae3e874395adc5229f0fe5cfcc111cc8c", 51 | "checksumType": "sha256", 52 | "bootConfig": "grub", 53 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 54 | "mirrors": [ 55 | { 56 | "name": "Default", 57 | "url": "https://download.fedoraproject.org/pub/fedora/linux/releases/36/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-36-1.5.iso" 58 | } 59 | ] 60 | }, 61 | "Kali": { 62 | "isoPattern": "^kali-linux-.+", 63 | "checksum": "f87618a6df20b6fdf4edebee1c6f1d808dee075a431229b3f75a5208e3c9c0e8", 64 | "checksumType": "sha256", 65 | "bootConfig": "grub", 66 | "kernelParams": "findiso={{.IsoPath}}", 67 | "mirrors": [ 68 | { 69 | "name": "Default", 70 | "url": "https://cdimage.kali.org/kali-2022.3/kali-linux-2022.3-live-amd64.iso" 71 | } 72 | ] 73 | }, 74 | "Linux Mint": { 75 | "isoPattern": "^linuxmint-.+", 76 | "checksum": "f524114e4a10fb04ec428af5e8faf7998b18271ea72fbb4b63efe0338957c0f3", 77 | "checksumType": "sha256", 78 | "bootConfig": "grub", 79 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 80 | "mirrors": [ 81 | { 82 | "name": "Default", 83 | "url": "https://mirrors.edge.kernel.org/linuxmint/stable/21/linuxmint-21-cinnamon-64bit.iso" 84 | } 85 | ] 86 | }, 87 | "Manjaro": { 88 | "isoPattern": "^manjaro-.+", 89 | "checksum": "63b76319e4ca91d626e2bd30d34e841e134baec9", 90 | "checksumType": "sha1", 91 | "kernelParams": "img_dev=/dev/disk/by-uuid/{{.UUID}} img_loop={{.IsoPath}}", 92 | "customConfigs": [ 93 | { 94 | "Label": "Default Config", 95 | "KernelPath": "/boot/vmlinuz-x86_64", 96 | "InitrdPath": "/boot/initramfs-x86_64.img", 97 | "Cmdline": "driver=free tz=utc lang=en_US keytable=en" 98 | } 99 | ], 100 | "mirrors": [ 101 | { 102 | "name": "Default", 103 | "url": "https://download.manjaro.org/xfce/21.3.7/manjaro-xfce-21.3.7-220816-linux515.iso" 104 | } 105 | ] 106 | }, 107 | "TinyCore": { 108 | "isoPattern": ".*CorePure64-.+", 109 | "checksum": "84b488347246ac9ded4c4a09c3800306", 110 | "checksumType": "md5", 111 | "bootConfig": "syslinux", 112 | "kernelParams": "iso=UUID={{.UUID}}{{.IsoPath}} console=ttyS0 earlyprintk=ttyS0", 113 | "mirrors": [ 114 | { 115 | "name": "Default", 116 | "url": "http://tinycorelinux.net/13.x/x86_64/release/TinyCorePure64-13.1.iso" 117 | } 118 | ] 119 | }, 120 | "LHSCowboys": { 121 | "isoPattern": ".*CorePure64-.+", 122 | "bootConfig": "syslinux", 123 | "kernelParams": "iso=UUID={{.UUID}}{{.IsoPath}}", 124 | "mirrors": [ 125 | { 126 | "name": "Default", 127 | "url": "https://github.com/u-root/webboot-distro/raw/master/iso/tinycore/10.x/x86_64/release/LHSCowboys.iso" 128 | } 129 | ] 130 | }, 131 | "DHSGaels": { 132 | "isoPattern": ".*CorePure64-.+", 133 | "bootConfig": "syslinux", 134 | "kernelParams": "iso=UUID={{.UUID}}{{.IsoPath}}", 135 | "mirrors": [ 136 | { 137 | "name": "Default", 138 | "url": "https://github.com/u-root/webboot-distro/raw/master/iso/tinycore/10.x/x86_64/release/LHSCowboys.iso" 139 | } 140 | ] 141 | }, 142 | "Ubuntu": { 143 | "isoPattern": "^ubuntu-.+", 144 | "checksum": "c396e956a9f52c418397867d1ea5c0cf1a99a49dcf648b086d2fb762330cc88d", 145 | "checksumType": "sha256", 146 | "bootConfig": "syslinux", 147 | "kernelParams": "iso-scan/filename={{.IsoPath}}", 148 | "mirrors": [ 149 | { 150 | "name": "Default", 151 | "url": "https://releases.ubuntu.com/jammy/ubuntu-22.04.1-desktop-amd64.iso" 152 | } 153 | ] 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /cmds/webboot/network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | ui "github.com/gizak/termui/v3" 11 | "github.com/u-root/webboot/pkg/menu" 12 | "github.com/u-root/webboot/pkg/wifi" 13 | "github.com/vishvananda/netlink" 14 | ) 15 | 16 | // Collect stdout and stderr from the network setup. 17 | // Declare globally because wifi.Connect() triggers 18 | // go routines that might still be running after return. 19 | var wifiStdout, wifiStderr bytes.Buffer 20 | 21 | func connected() bool { 22 | client := http.Client{ 23 | Timeout: 10 * time.Second, 24 | } 25 | 26 | if _, err := client.Get("http://google.com"); err != nil { 27 | return false 28 | } 29 | return true 30 | } 31 | 32 | func wirelessIfaceEntries() ([]menu.Entry, error) { 33 | interfaces, err := netlink.LinkList() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | var ifEntries []menu.Entry 39 | for _, iface := range interfaces { 40 | if interfaceIsWireless(iface.Attrs().Name) { 41 | ifEntries = append(ifEntries, &Interface{label: iface.Attrs().Name}) 42 | } 43 | } 44 | return ifEntries, nil 45 | } 46 | 47 | func interfaceIsWireless(ifname string) bool { 48 | devPath := fmt.Sprintf("/sys/class/net/%s/wireless", ifname) 49 | if _, err := os.Stat(devPath); err != nil { 50 | return false 51 | } 52 | return true 53 | } 54 | 55 | func setupNetwork(uiEvents <-chan ui.Event, menus chan<- string) error { 56 | iface, err := selectNetworkInterface(uiEvents, menus) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return selectWirelessNetwork(uiEvents, menus, iface.Label()) 62 | } 63 | 64 | func selectNetworkInterface(uiEvents <-chan ui.Event, menus chan<- string) (menu.Entry, error) { 65 | ifEntries, err := wirelessIfaceEntries() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | iface, err := menu.PromptMenuEntry("Network Interfaces", "Choose an option", ifEntries, uiEvents, menus) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return iface, nil 76 | } 77 | 78 | func selectWirelessNetwork(uiEvents <-chan ui.Event, menus chan<- string, iface string) error { 79 | worker, err := wifi.NewIWLWorker(&wifiStdout, &wifiStderr, iface) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | for { 85 | progress := menu.NewProgress("Scanning for wifi networks", true) 86 | networkScan, err := worker.Scan(&wifiStdout, &wifiStderr) 87 | progress.Close() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | netEntries := []menu.Entry{} 93 | for _, network := range networkScan { 94 | netEntries = append(netEntries, &Network{info: network}) 95 | } 96 | 97 | entry, err := menu.PromptMenuEntry("Wireless Networks", "Choose an option", netEntries, uiEvents, menus) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | network, ok := entry.(*Network) 103 | if !ok { 104 | return fmt.Errorf("Bad menu entry.") 105 | } 106 | 107 | if err := connectWirelessNetwork(uiEvents, menus, worker, network.info); err != nil { 108 | switch err { 109 | case menu.ExitRequest: // user typed to exit 110 | return err 111 | case menu.BackRequest: // user typed to go back 112 | continue 113 | default: // connection error 114 | menu.DisplayResult([]string{err.Error()}, uiEvents, menus) 115 | continue 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | } 122 | 123 | func connectWirelessNetwork(uiEvents <-chan ui.Event, menus chan<- string, worker wifi.WiFi, network wifi.Option) error { 124 | var setupParams = []string{network.Essid} 125 | authSuite := network.AuthSuite 126 | 127 | if authSuite == wifi.NotSupportedProto { 128 | return fmt.Errorf("Security protocol is not supported.") 129 | } else if authSuite == wifi.WpaPsk || authSuite == wifi.WpaEap { 130 | credentials, err := enterCredentials(uiEvents, menus, authSuite) 131 | if err != nil { 132 | return err 133 | } 134 | setupParams = append(setupParams, credentials...) 135 | } 136 | 137 | progress := menu.NewProgress("Connecting to network", true) 138 | err := worker.Connect(&wifiStdout, &wifiStderr, setupParams...) 139 | progress.Close() 140 | if err != nil { 141 | return err 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func enterCredentials(uiEvents <-chan ui.Event, menus chan<- string, authSuite wifi.SecProto) ([]string, error) { 148 | var credentials []string 149 | pass, err := menu.PromptTextInput("Enter password:", menu.AlwaysValid, uiEvents, menus) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | credentials = append(credentials, pass) 155 | if authSuite == wifi.WpaPsk { 156 | return credentials, nil 157 | } 158 | 159 | // If not WpaPsk, the network uses WpaEap and also needs an identity 160 | identity, err := menu.PromptTextInput("Enter identity:", menu.AlwaysValid, uiEvents, menus) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | credentials = append(credentials, identity) 166 | return credentials, nil 167 | } 168 | -------------------------------------------------------------------------------- /cmds/webboot/testdata/dirlevel1/dirlevel2/TinyCorePure64.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-root/webboot/60bfe13edb27b6e90e4a060a4d1c30fe12307a6c/cmds/webboot/testdata/dirlevel1/dirlevel2/TinyCorePure64.iso -------------------------------------------------------------------------------- /cmds/webboot/testdata/dirlevel1/fakeDistro.iso: -------------------------------------------------------------------------------- 1 | This is a fake ISO. 2 | -------------------------------------------------------------------------------- /cmds/webboot/types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/u-root/u-root/pkg/boot" 7 | "github.com/u-root/u-root/pkg/mount/block" 8 | "github.com/u-root/webboot/pkg/bootiso" 9 | "github.com/u-root/webboot/pkg/menu" 10 | "github.com/u-root/webboot/pkg/wifi" 11 | ) 12 | 13 | type Distro struct { 14 | IsoPattern string 15 | Checksum string 16 | ChecksumType string 17 | BootConfig string 18 | KernelParams string 19 | CustomConfigs []bootiso.Config 20 | Mirrors []Mirror 21 | } 22 | 23 | type Mirror struct { 24 | Name string 25 | Url string 26 | } 27 | 28 | func (m *Mirror) Label() string { 29 | return m.Name 30 | } 31 | 32 | var supportedDistros = map[string]Distro{} 33 | 34 | type CacheDevice struct { 35 | Name string 36 | UUID string 37 | MountPoint string 38 | IsoPath string // set after iso is selected 39 | } 40 | 41 | func NewCacheDevice(device *block.BlockDev, mountPoint string) CacheDevice { 42 | return CacheDevice{ 43 | Name: device.Name, 44 | UUID: device.FsUUID, 45 | MountPoint: mountPoint, 46 | } 47 | } 48 | 49 | // ISO contains information of the iso user wants to boot. 50 | type ISO struct { 51 | label string 52 | path string 53 | checksum string 54 | } 55 | 56 | var _ = menu.Entry(&ISO{}) 57 | 58 | // Label is the string this iso displays in the menu page. 59 | func (i *ISO) Label() string { 60 | return i.label 61 | } 62 | 63 | // Config represents one kind of configure of booting an iso. 64 | type Config struct { 65 | label string 66 | } 67 | 68 | var _ = menu.Entry(&Config{}) 69 | 70 | // Label is the string this iso displays in the menu page. 71 | func (c *Config) Label() string { 72 | return c.label 73 | } 74 | 75 | // DownloadOption lets the user download an iso then boot it. 76 | type DownloadOption struct { 77 | } 78 | 79 | var _ = menu.Entry(&DownloadOption{}) 80 | 81 | // Label is the string this iso displays in the menu page. 82 | func (d *DownloadOption) Label() string { 83 | return "Download an ISO" 84 | } 85 | 86 | // DirOption represents a directory under cache directory. 87 | // It displays its sub-directory or iso files. 88 | type DirOption struct { 89 | label string 90 | path string 91 | } 92 | 93 | var _ = menu.Entry(&DirOption{}) 94 | 95 | // Label is the string this option displays in the menu page. 96 | func (d *DirOption) Label() string { 97 | return d.label 98 | } 99 | 100 | type Interface struct { 101 | label string 102 | } 103 | 104 | func (i *Interface) Label() string { 105 | return i.label 106 | } 107 | 108 | type Network struct { 109 | info wifi.Option 110 | } 111 | 112 | func (n *Network) Label() string { 113 | switch n.info.AuthSuite { 114 | case wifi.NoEnc: 115 | return fmt.Sprintf("%s: No Passphrase\n", n.info.Essid) 116 | case wifi.WpaPsk: 117 | return fmt.Sprintf("%s: WPA-PSK (only passphrase)\n", n.info.Essid) 118 | case wifi.WpaEap: 119 | return fmt.Sprintf("%s: WPA-EAP (passphrase and identity)\n", n.info.Essid) 120 | case wifi.NotSupportedProto: 121 | return fmt.Sprintf("%s: Not a supported protocol\n", n.info.Essid) 122 | } 123 | return "Invalid wifi network." 124 | } 125 | 126 | type BootConfig struct { 127 | image boot.OSImage 128 | } 129 | 130 | func (b *BootConfig) Label() string { 131 | return b.image.Label() 132 | } 133 | -------------------------------------------------------------------------------- /cmds/webboot/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "regexp" 11 | "sort" 12 | 13 | ui "github.com/gizak/termui/v3" 14 | "github.com/u-root/webboot/pkg/menu" 15 | ) 16 | 17 | // WriteCounter counts the number of bytes written to it. It implements an io.Writer 18 | type WriteCounter struct { 19 | received float64 20 | expected float64 21 | progress menu.Progress 22 | } 23 | 24 | func NewWriteCounter(expectedSize int64) WriteCounter { 25 | return WriteCounter{0, float64(expectedSize), menu.NewProgress("", false)} 26 | } 27 | 28 | func (wc *WriteCounter) Write(p []byte) (int, error) { 29 | n := len(p) 30 | wc.received += float64(n) 31 | wc.progress.Update(fmt.Sprintf("Downloading... %.2f%% (%.3f MB)\n\nPress to cancel.", 100*(wc.received/wc.expected), wc.received/1000000)) 32 | return n, nil 33 | } 34 | 35 | func (wc *WriteCounter) Close() { 36 | wc.progress.Close() 37 | } 38 | 39 | // download() will download a file from URL and save it to a temp file 40 | // If the download succeeds, the temp file will be copied to fPath 41 | func download(URL, fPath, downloadDir string, uiEvents <-chan ui.Event) error { 42 | ctx, cancel := context.WithCancel(context.Background()) 43 | defer cancel() 44 | 45 | req, err := http.NewRequestWithContext(ctx, "GET", URL, nil) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | resp, err := http.DefaultClient.Do(req) 51 | if err != nil { 52 | return err 53 | } 54 | if resp.StatusCode != 200 { 55 | return fmt.Errorf("Received http status code %s", resp.Status) 56 | } 57 | defer resp.Body.Close() 58 | 59 | tempFile, err := ioutil.TempFile(downloadDir, "iso-download-") 60 | if err != nil { 61 | return err 62 | } 63 | defer os.Remove(tempFile.Name()) 64 | 65 | go listenForCancel(ctx, cancel, uiEvents) 66 | counter := NewWriteCounter(resp.ContentLength) 67 | 68 | if _, err = io.Copy(tempFile, io.TeeReader(resp.Body, &counter)); err != nil { 69 | counter.Close() 70 | return err 71 | } 72 | 73 | counter.Close() 74 | copyProgress := menu.NewProgress(fmt.Sprintf("Download complete. Writing ISO to cache (%q)", fPath), true) 75 | defer copyProgress.Close() 76 | 77 | if _, err = tempFile.Seek(0, 0); err != nil { 78 | return err 79 | } 80 | 81 | err = os.Rename(tempFile.Name(), fPath) 82 | if err != nil { 83 | return fmt.Errorf("Error on os.Rename: %v", err) 84 | } 85 | 86 | verbose("%q is downloaded at %q\n", URL, fPath) 87 | return nil 88 | } 89 | 90 | func listenForCancel(ctx context.Context, cancel context.CancelFunc, uiEvents <-chan ui.Event) { 91 | for { 92 | select { 93 | case k := <-uiEvents: 94 | if k.ID == "" { 95 | cancel() 96 | return 97 | } 98 | case <-ctx.Done(): 99 | return 100 | } 101 | } 102 | } 103 | 104 | func inferIsoType(isoName string, supportedDistros map[string]Distro) string { 105 | for distroName, distroInfo := range supportedDistros { 106 | match, _ := regexp.MatchString(distroInfo.IsoPattern, isoName) 107 | if match { 108 | return distroName 109 | } 110 | } 111 | return "" 112 | } 113 | 114 | func supportedDistroEntries() []menu.Entry { 115 | entries := []menu.Entry{} 116 | for distroName := range supportedDistros { 117 | entries = append(entries, &Config{label: distroName}) 118 | } 119 | 120 | sort.Slice(entries[:], func(i, j int) bool { 121 | return entries[i].Label() < entries[j].Label() 122 | }) 123 | 124 | return entries 125 | } 126 | 127 | func validURL(url string, ext string) (string, string, bool) { 128 | match, _ := regexp.MatchString(fmt.Sprintf("^https*://.+\\.%s$", ext), url) 129 | if match { 130 | return url, "", true 131 | } else { 132 | return url, "Invalid URL.", false 133 | } 134 | } 135 | 136 | func validIso(url string) (string, string, bool) { 137 | return validURL(url, "iso") 138 | } 139 | 140 | func validJson(url string) (string, string, bool) { 141 | return validURL(url, "json") 142 | } 143 | -------------------------------------------------------------------------------- /cmds/webboot/webboot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "html/template" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "path" 14 | "path/filepath" 15 | "strings" 16 | 17 | ui "github.com/gizak/termui/v3" 18 | Boot "github.com/u-root/u-root/pkg/boot" 19 | "github.com/u-root/u-root/pkg/mount" 20 | "github.com/u-root/u-root/pkg/mount/block" 21 | "github.com/u-root/webboot/pkg/bootiso" 22 | "github.com/u-root/webboot/pkg/menu" 23 | ) 24 | 25 | var ( 26 | v = flag.Bool("verbose", false, "Verbose output") 27 | verbose = func(string, ...interface{}) {} 28 | dir = flag.String("dir", "", "Path of cached directory") 29 | network = flag.Bool("network", true, "If network is false we will not set up network") 30 | dryRun = flag.Bool("dryrun", false, "If dry_run is true we won't boot the iso.") 31 | cacheDev CacheDevice 32 | logBuffer bytes.Buffer 33 | tmpBuffer bytes.Buffer 34 | ) 35 | 36 | const jsonURL = "https://raw.githubusercontent.com/u-root/webboot/main/cmds/webboot/distros.json" 37 | 38 | // ISO's exec downloads the iso and boot it. 39 | func (i *ISO) exec(uiEvents <-chan ui.Event, menus chan<- string, boot bool) error { 40 | verbose("Intent to boot %s", i.path) 41 | 42 | distroName := inferIsoType(path.Base(i.path), supportedDistros) 43 | distro, ok := supportedDistros[distroName] 44 | 45 | if !ok { 46 | // Could not infer ISO type based on filename 47 | // Prompt user to identify the ISO's type 48 | entries := supportedDistroEntries() 49 | entry, err := menu.PromptMenuEntry("ISO Type", "Select the closest distribution:", entries, uiEvents, menus) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | distro = supportedDistros[entry.Label()] 55 | } 56 | 57 | verbose("Using distro %s with boot config %s", distroName, distro.BootConfig) 58 | 59 | var configs []Boot.OSImage 60 | if distro.BootConfig != "" { 61 | parsedConfigs, err := bootiso.ParseConfigFromISO(i.path, distro.BootConfig) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | configs = append(configs, parsedConfigs...) 67 | } 68 | 69 | if len(distro.CustomConfigs) != 0 { 70 | CustomConfigs, err := bootiso.LoadCustomConfigs(i.path, distro.CustomConfigs) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | configs = append(configs, CustomConfigs...) 76 | } 77 | 78 | if len(configs) == 0 { 79 | return fmt.Errorf("No valid configs were found.") 80 | } 81 | 82 | verbose("Get configs: %+v", configs) 83 | 84 | entries := []menu.Entry{} 85 | for _, config := range configs { 86 | entries = append(entries, &BootConfig{config}) 87 | } 88 | 89 | entry, err := menu.PromptMenuEntry("Configs", "Choose an option", entries, uiEvents, menus) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | config, ok := entry.(*BootConfig) 95 | if !ok { 96 | return fmt.Errorf("Could not convert selection to a boot image.") 97 | } 98 | 99 | if err == nil { 100 | cacheDev.IsoPath = strings.ReplaceAll(i.path, cacheDev.MountPoint, "") 101 | paramTemplate, err := template.New("template").Parse(distro.KernelParams) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | var kernelParams bytes.Buffer 107 | if err = paramTemplate.Execute(&kernelParams, cacheDev); err != nil { 108 | return err 109 | } 110 | 111 | if !boot { 112 | s := fmt.Sprintf("config.image %s, kernelparams.String() %s", config.image, kernelParams.String()) 113 | return fmt.Errorf("Booting is disabled (see --dryrun flag), but otherwise would be [%s].", s) 114 | } 115 | err = bootiso.BootCachedISO(config.image, kernelParams.String()+" waitusb=10") 116 | } 117 | 118 | // If kexec succeeds, we should not arrive here 119 | if err == nil { 120 | // TODO: We should know whether we tried using /sbin/kexec. 121 | err = fmt.Errorf("kexec failed, but gave no error. Consider trying kexec-tools.") 122 | } 123 | 124 | return err 125 | } 126 | 127 | // DownloadOption's exec lets user input the name of the iso they want 128 | // if this iso is existed in the bookmark, use it's url 129 | // elsewise ask for a download link 130 | func (d *DownloadOption) exec(uiEvents <-chan ui.Event, menus chan<- string, network bool, cacheDir string) (menu.Entry, error) { 131 | 132 | entries := supportedDistroEntries() 133 | customLabel := "Other Distro" 134 | entries = append(entries, &Config{customLabel}) 135 | entry, err := menu.PromptMenuEntry("Linux Distros", "Choose an option:", entries, uiEvents, menus) 136 | if err != nil { 137 | return nil, err 138 | } 139 | var link string 140 | 141 | if entry.Label() == customLabel { 142 | link, err = menu.PromptTextInput("Enter URL:", validIso, uiEvents, menus) 143 | if err != nil { 144 | return nil, err 145 | } 146 | } else { 147 | link, _, err = mirrorMenu(entry, uiEvents, menus, link) 148 | if err != nil { 149 | return nil, err 150 | } 151 | } 152 | 153 | filename := path.Base(link) 154 | 155 | // If the cachedir is not find, downloaded the iso to /tmp, else create a Downloaded dir in the cache dir. 156 | var fpath string 157 | var downloadDir string 158 | 159 | if cacheDir == "" { 160 | downloadDir = os.TempDir() 161 | fpath = filepath.Join(downloadDir, filename) 162 | } else { 163 | downloadDir = filepath.Join(cacheDir, "Downloaded") 164 | if err = os.MkdirAll(downloadDir, os.ModePerm); err != nil { 165 | return nil, fmt.Errorf("Fail to create the downloaded dir :%v", err) 166 | } 167 | fpath = filepath.Join(downloadDir, filename) 168 | } 169 | 170 | if err = download(link, fpath, downloadDir, uiEvents); err != nil { 171 | if err == context.Canceled { 172 | return nil, fmt.Errorf("Download was canceled.") 173 | } else { 174 | return nil, err 175 | } 176 | } 177 | 178 | menu, err := displayChecksumPrompt(uiEvents, menus, supportedDistros, entry.Label(), fpath) 179 | if err != nil { 180 | return nil, err 181 | } else if menu != nil { 182 | return menu, nil 183 | } 184 | 185 | return &ISO{label: filename, path: fpath}, nil 186 | } 187 | 188 | // DirOption's exec displays subdirectory or cached isos under the path directory 189 | func (d *DirOption) exec(uiEvents <-chan ui.Event, menus chan<- string) (menu.Entry, error) { 190 | entries := []menu.Entry{} 191 | readerInfos, err := ioutil.ReadDir(d.path) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | // check the directory, if there is a subdirectory, add a DirOption option to next menu 197 | // if there is iso file, add an ISO option 198 | for _, info := range readerInfos { 199 | if info.IsDir() { 200 | entries = append(entries, &DirOption{ 201 | label: info.Name(), 202 | path: filepath.Join(d.path, info.Name()), 203 | }) 204 | } else if filepath.Ext(info.Name()) == ".iso" { 205 | iso := &ISO{ 206 | path: filepath.Join(d.path, info.Name()), 207 | label: info.Name(), 208 | } 209 | entries = append(entries, iso) 210 | } 211 | } 212 | 213 | return menu.PromptMenuEntry("Distros", "Choose an option:", entries, uiEvents, menus) 214 | } 215 | 216 | // getJsonLink prompts users to choose or enter the url for the JSON file that will be used. 217 | // It returns a string url, a bool telling whether or not the file needs to be downloaded, and an error. 218 | func getJsonLink(uiEvents <-chan ui.Event, menus chan<- string) (string, bool, error) { 219 | entries := []menu.Entry{ 220 | &Config{label: "Downloaded (recommended)"}, 221 | &Config{label: "Local"}, 222 | &Config{label: "Enter a custom URL"}, 223 | } 224 | 225 | entry, err := menu.PromptMenuEntry("Which list of distros would you like to choose from?", "Select an option.", entries, uiEvents, menus) 226 | if err != nil { 227 | return "", false, fmt.Errorf("Failed to display PromptMenuEntry: %v", err) 228 | } 229 | 230 | switch entry.Label() { 231 | case "Downloaded (recommended)": 232 | return jsonURL, true, nil 233 | case "Local": 234 | return "./distros.json", false, nil 235 | case "Enter a custom URL": 236 | // get user input 237 | customUrl, err := menu.PromptTextInput("Enter URL:", validJson, uiEvents, menus) 238 | if err != nil { 239 | return "", false, fmt.Errorf("Failed to display PromptMenuEntry: %v", err) 240 | } 241 | return customUrl, true, nil 242 | default: 243 | return "", false, fmt.Errorf("No valid option chosen.") 244 | } 245 | } 246 | 247 | // distroData downloads and parses the data in distros.json to a map[string]Distro. 248 | func distroData(uiEvents <-chan ui.Event, menus chan<- string, cacheDir string) (map[string]Distro, error) { 249 | jsonPath := "./distros.json" 250 | 251 | // Get the download link. 252 | jsonLink, needDownload, err := getJsonLink(uiEvents, menus) 253 | if err != nil { 254 | return nil, fmt.Errorf("Error in getJsonLink: %v", err) 255 | } 256 | 257 | if needDownload { 258 | var downloadDir string 259 | 260 | if cacheDir == "" { 261 | downloadDir = os.TempDir() 262 | jsonPath = filepath.Join(downloadDir, "distros.json") 263 | } else { 264 | downloadDir = filepath.Join(cacheDir, "Downloaded") 265 | if err := os.MkdirAll(downloadDir, os.ModePerm); err != nil { 266 | return nil, fmt.Errorf("Fail to create the downloaded dir: %v", err) 267 | } 268 | jsonPath = filepath.Join(downloadDir, "distros.json") 269 | } 270 | 271 | // Download the json file. 272 | if err := download(jsonLink, jsonPath, downloadDir, uiEvents); err != nil { 273 | if err == context.Canceled { 274 | return nil, fmt.Errorf("JSON file download was canceled.") 275 | } else { 276 | entries := []menu.Entry{&Config{label: "Ok"}} 277 | _, err := menu.PromptMenuEntry("Failed to download JSON file.", "Choose \"Ok\" to proceed using default JSON file.", entries, uiEvents, menus) 278 | if err != nil { 279 | return nil, fmt.Errorf("Could not display PromptMenuEntry: %v", err) 280 | } 281 | jsonPath = "./distros.json" 282 | } 283 | } 284 | } 285 | 286 | // Parse the json file. 287 | data, err := ioutil.ReadFile(jsonPath) 288 | 289 | if err != nil { 290 | return nil, fmt.Errorf("Could not read JSON file: %v\n", err) 291 | } 292 | 293 | supportedDistros := map[string]Distro{} 294 | 295 | err = json.Unmarshal([]byte(data), &supportedDistros) 296 | if err != nil { 297 | return nil, fmt.Errorf("Could not unmarshal JSON file: %v\n", err) 298 | } 299 | 300 | return supportedDistros, nil 301 | } 302 | 303 | // If the chosen distro has a checksum, verify it. 304 | // If the checksum is not correct, prompt the user to choose whether they still want to continue. 305 | func displayChecksumPrompt(uiEvents <-chan ui.Event, menus chan<- string, supportedDistros map[string]Distro, label string, fpath string) (menu.Entry, error) { 306 | // Check that the distro is supported 307 | if _, ok := supportedDistros[label]; ok { 308 | distro := supportedDistros[label] 309 | // Check that checksum is available 310 | if distro.Checksum == "" { 311 | accept, err := menu.PromptConfirmation("This distro does not have a checksum. Proceed anyway?", uiEvents, menus) 312 | if err != nil { 313 | return nil, fmt.Errorf("Failed to prompt confirmation: %s", err) 314 | } 315 | if !accept { 316 | // Go back to download menu 317 | return &DownloadOption{}, nil 318 | } 319 | } else if valid, calcChecksum, err := bootiso.VerifyChecksum(fpath, distro.Checksum, distro.ChecksumType); err != nil { 320 | return nil, fmt.Errorf("Failed to verify checksum: %s", err) 321 | } else if !valid { 322 | accept, err := menu.PromptConfirmation(fmt.Sprintf("Checksum was not correct. The correct checksum is %s and the downloaded ISO's checksum is %s. Proceed anyway?", 323 | distro.Checksum, calcChecksum), uiEvents, menus) 324 | if err != nil { 325 | return nil, fmt.Errorf("Failed to prompt confirmation: %s", err) 326 | } 327 | if !accept { 328 | // Go back to download menu 329 | return &DownloadOption{}, nil 330 | } 331 | } 332 | } 333 | return nil, nil 334 | } 335 | 336 | // mirrorMenu fetches the mirror options of the distro the user selects and displays them in a new menu. Finally, it gets 337 | // the download link of the mirror the user selects. 338 | func mirrorMenu(entry menu.Entry, uiEvents <-chan ui.Event, menus chan<- string, link string) (url string, mirrorNameForTestPurposes string, err error) { 339 | // Code for after the specific distro has been selected. 340 | // Looks up the distro. 341 | distro := supportedDistros[entry.Label()] 342 | if len(distro.Mirrors) > 0 { 343 | // Make an array of type menu.Entry to store the mirrors of the 344 | // particular distro selected. Then, display the mirror options. 345 | entries := make([]menu.Entry, len(distro.Mirrors)) 346 | for i := range entries { 347 | entries[i] = &distro.Mirrors[i] 348 | } 349 | entry, err = menu.PromptMenuEntry("Available Mirrors", "Choose an option:", entries, uiEvents, menus) 350 | if err != nil { 351 | return "", "", err 352 | } 353 | } 354 | // Iterate through the mirrors of the distro to select the appropriate link. 355 | for i := range distro.Mirrors { 356 | if distro.Mirrors[i].Name == entry.Label() { 357 | link = distro.Mirrors[i].Url 358 | return link, distro.Mirrors[i].Name, err 359 | } 360 | } 361 | return "", "", fmt.Errorf("Mirror not found: %v", entry.Label()) 362 | } 363 | 364 | // getCachedDirectory recognizes the usb stick that contains the cached directory from block devices, 365 | // and return the path of cache dir. 366 | // the cache dir should locate at the root of USB stick and be named as "Images" 367 | // +-- USB root 368 | // | +-- Images (<--- the cache directory) 369 | // | +-- subdirectories or iso files 370 | // ... 371 | func getCachedDirectory() (string, error) { 372 | blockDevs, err := block.GetBlockDevices() 373 | if err != nil { 374 | return "", fmt.Errorf("No available block devices to boot from") 375 | } 376 | 377 | mountPoints, err := ioutil.TempDir("", "temp-device-") 378 | if err != nil { 379 | return "", fmt.Errorf("Cannot create tmpdir: %v", err) 380 | } 381 | 382 | for _, device := range blockDevs { 383 | mp, err := mount.TryMount(filepath.Join("/dev/", device.Name), filepath.Join(mountPoints, device.Name), "", 0) 384 | if err != nil { 385 | continue 386 | } 387 | cachePath := filepath.Join(mp.Path, "Images") 388 | if _, err = os.Stat(cachePath); err == nil { 389 | cacheDev = NewCacheDevice(device, mp.Path) 390 | return cachePath, nil 391 | } 392 | } 393 | return "", fmt.Errorf("Do not find the cache directory: Expected a /Images under at the root of a block device(USB)") 394 | } 395 | 396 | type LogOption struct { 397 | } 398 | 399 | func (d *LogOption) Label() string { 400 | return "Show last log" 401 | } 402 | 403 | func getMainMenu(cacheDir string, menus chan<- string) menu.Entry { 404 | entries := []menu.Entry{} 405 | if cacheDir != "" { 406 | // UseCacheOption is a special DirOption represents the root of cache dir 407 | entries = append(entries, &DirOption{label: "Use Cached ISO", path: cacheDir}) 408 | } 409 | entries = append(entries, &DownloadOption{}) 410 | entries = append(entries, &LogOption{}) 411 | 412 | for { 413 | // Display the main menu until user makes a valid choice or 414 | // they encounter an error that's not menu.BackRequest 415 | entry, err := menu.PromptMenuEntry("Webboot", "Choose an option:", entries, ui.PollEvents(), menus) 416 | if err != nil && err != menu.BackRequest { 417 | log.Fatal(err) 418 | } else if entry != nil { 419 | return entry 420 | } 421 | } 422 | } 423 | 424 | func handleError(err error, menus chan<- string) { 425 | if err == menu.ExitRequest { 426 | menu.Close() 427 | os.Exit(0) 428 | } else if err == menu.BackRequest { 429 | return 430 | } 431 | 432 | errorText := err.Error() + "\n" + tmpBuffer.String() + wifiStdout.String() + wifiStderr.String() 433 | fmt.Fprintln(&logBuffer, errorText) 434 | menu.DisplayResult(strings.Split(errorText, "\n"), ui.PollEvents(), menus) 435 | 436 | tmpBuffer.Reset() 437 | wifiStdout.Reset() 438 | wifiStderr.Reset() 439 | } 440 | 441 | func showLog(menus chan<- string) { 442 | s := logBuffer.String() 443 | if len(s) > 1024 { 444 | s = s[len(s)-1024:] 445 | } 446 | menu.DisplayResult(strings.Split(s, "\n"), ui.PollEvents(), menus) 447 | } 448 | 449 | func main() { 450 | 451 | flag.Parse() 452 | if *v { 453 | verbose = log.Printf 454 | } 455 | 456 | cacheDir := *dir 457 | if cacheDir != "" { 458 | // call filepath.Clean to make sure the format of path is consistent 459 | // we should check the cacheDir != "" before call filepath.Clean, because filepath.Clean("") = "." 460 | cacheDir = filepath.Clean(cacheDir) 461 | } else { 462 | if cachePath, err := getCachedDirectory(); err != nil { 463 | verbose("Fail to find the USB stick: %+v", err) 464 | } else { 465 | cacheDir = cachePath 466 | } 467 | } 468 | verbose("Using cache dir: %v", cacheDir) 469 | if err := menu.Init(); err != nil { 470 | log.Fatalf(err.Error()) 471 | } 472 | 473 | menus := make(chan string) 474 | // Continuously throw away values from menus channel so that the channel doesn't block. 475 | go func() { 476 | for { 477 | <-menus 478 | } 479 | }() 480 | entry := getMainMenu(cacheDir, menus) 481 | 482 | // Buffer the log output, else it might overlap with the menu 483 | log.SetOutput(&tmpBuffer) 484 | 485 | // check the chosen entry of each level 486 | // and call it's exec() to get the next level's chosen entry. 487 | // repeat this process until there is no next level 488 | var err error 489 | 490 | for entry != nil { 491 | switch entry.(type) { 492 | case *LogOption: 493 | showLog(menus) 494 | entry = getMainMenu(cacheDir, menus) 495 | case *DownloadOption: 496 | // set up network 497 | progress := menu.NewProgress("Testing network connection", true) 498 | activeConnection := connected() 499 | progress.Close() 500 | 501 | if *network && !activeConnection { 502 | if err := setupNetwork(ui.PollEvents(), menus); err != nil { 503 | verbose("error on setupNetwork: %+v", err) 504 | } 505 | } 506 | 507 | // get distro data 508 | supportedDistros, err = distroData(ui.PollEvents(), menus, cacheDir) 509 | if err != nil { 510 | log.Fatalf("Error on supportedDistros(): %v", err.Error()) 511 | } 512 | 513 | if entry, err = entry.(*DownloadOption).exec(ui.PollEvents(), menus, *network, cacheDir); err != nil { 514 | handleError(err, menus) 515 | entry = getMainMenu(cacheDir, menus) 516 | } 517 | case *ISO: 518 | if err = entry.(*ISO).exec(ui.PollEvents(), menus, !*dryRun); err != nil { 519 | handleError(err, menus) 520 | entry = getMainMenu(cacheDir, menus) 521 | } 522 | case *DirOption: 523 | dirOption := entry.(*DirOption) 524 | if entry, err = dirOption.exec(ui.PollEvents(), menus); err != nil { 525 | // Check if user requested to go back from a cache subdirectory, 526 | // so we can send them to a DirOption for the parent directory 527 | if err == menu.BackRequest && dirOption.path != cacheDir { 528 | entry = &DirOption{path: filepath.Dir(dirOption.path)} 529 | } else { 530 | // Otherwise they either requested to go back from the 531 | // cache root, so we can send them to main menu, 532 | // or they encountered an error 533 | handleError(err, menus) 534 | entry = getMainMenu(cacheDir, menus) 535 | } 536 | } 537 | default: 538 | handleError(fmt.Errorf("Unknown menu type %T!\n", entry), menus) 539 | entry = getMainMenu(cacheDir, menus) 540 | } 541 | } 542 | } 543 | -------------------------------------------------------------------------------- /cmds/wifiDebug/wifiDebug.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package main takes the connect function of iwl.go and reduces it 6 | // to as few lines as possible to make spotting errors in wifi bugs easier. 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | ) 14 | 15 | // IWLWorker implements the WiFi interface using the Intel Wireless LAN commands 16 | type IWLWorker struct { 17 | Interface string 18 | } 19 | 20 | func main() { 21 | // There's no telling how long the supplicant will take, but on the other hand, 22 | // it's been almost instantaneous. But, further, it needs to keep running. 23 | go func() { 24 | cmd := exec.Command("/usr/bin/strace", "-o", "/tmp/out", "wpa_supplicant", "-dd", "-i wlan0", "-c/tmp/wifi.conf") 25 | cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr 26 | fmt.Println("wpa supplicant cmd.Stdout: ", cmd.Stdout) 27 | fmt.Println("wpa supplicant cmd.Stderr: ", cmd.Stderr) 28 | err := cmd.Run() 29 | fmt.Println("wpa supplicant cmd.Run() error: ", err) 30 | }() 31 | 32 | // dhclient might never return on incorrect passwords or identity 33 | go func() { 34 | cmd := exec.Command("dhclient", "-ipv4=true", "-ipv6=false", "-v", "wlan0") 35 | cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr 36 | fmt.Println("dhclient cmd.Stdout: ", cmd.Stdout) 37 | fmt.Println("dhclient cmd.Stderr: ", cmd.Stderr) 38 | err := cmd.Run() 39 | fmt.Println("dhclient cmd.Run() error: ", err) 40 | }() 41 | 42 | for { 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /distros.md: -------------------------------------------------------------------------------- 1 | # Distributions of Linux-based systems 2 | 3 | Well... https://twitter.com/OrangeCMS/status/1220605490792751104 4 | 5 | This file lists details on how the respective distributions patch and configure 6 | their kernels, where to obtain the sources and config files, and the download 7 | URLs for current release ISO images. That helps maintaining and further 8 | developing webboot. 9 | 10 | ## Arch Linux 11 | 12 | - [ISO](http://mirror.rackspace.com/archlinux/iso/2020.01.01/archlinux-2020.01.01-x86_64.iso) 13 | - [kernel sources](https://git.archlinux.org/linux.git?signed#tag=v5.4.14-arch1) 14 | - [kernel config](https://git.archlinux.org/svntogit/packages.git/tree/trunk?h=packages/linux) 15 | 16 | ### SystemRescueCd 17 | 18 | Formerly based on Gentoo, SystemRescueCd now builds on top of Arch Linux. Hence, 19 | the directory structure in the ISO is similar to the Arch Linux ISO. 20 | 21 | It is meant to be a live distro for system rescue tasks, hence the name. 22 | 23 | - [download page](http://www.system-rescue-cd.org/Download/) 24 | - [ISO](https://osdn.net/projects/systemrescuecd/storage/releases/6.0.7/systemrescuecd-6.0.7.iso) 25 | 26 | ## Fedora 27 | 28 | [Build instructions](https://fedoraproject.org/wiki/Building_a_custom_kernel) 29 | in the wiki are specific to Fedora and not suitable for other systems. 30 | Cloning from the repository and applying the config should just work though. 31 | 32 | - [ISO](https://download.fedoraproject.org/pub/fedora/linux/releases/31/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-31-1.9.iso) 33 | - [kernel sources](https://src.fedoraproject.org/rpms/kernel/tree/master) 34 | - [stable sources](https://koji.fedoraproject.org/koji/search?terms=kernel-5.5.0-0.rc6.git3.1.fc32&type=build&match=glob) 35 | - [kernel config](https://src.fedoraproject.org/rpms/kernel/raw/master/f/kernel-x86_64-fedora.config) 36 | 37 | ### Release 30 38 | 39 | - [ISO](https://dl.fedoraproject.org/pub/archive/fedora/linux/releases/30/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-30-1.2.iso) 40 | - [netinst](https://dl.fedoraproject.org/pub/archive/fedora/linux/releases/30/Workstation/x86_64/iso/Fedora-Workstation-netinst-x86_64-30-1.2.iso) 41 | - [kernel sources](https://dl.fedoraproject.org/pub/archive/fedora/linux/releases/30/Workstation/source/tree/Packages/k/kernel-5.0.9-301.fc30.src.rpm) 42 | 43 | ## openSUSE 44 | 45 | Details on the kernel are [in the wiki](https://en.opensuse.org/Kernel), as well 46 | as [instructions](https://en.opensuse.org/openSUSE:Kernel_git). 47 | 48 | - [ISO](https://download.opensuse.org/distribution/leap/15.1/iso/openSUSE-Leap-15.1-DVD-x86_64.iso), 49 | [netboot](https://download.opensuse.org/distribution/leap/15.1/iso/openSUSE-Leap-15.1-NET-x86_64.iso) 50 | - [kernel sources](https://kernel.opensuse.org/cgit/kernel-source/tree/config/x86_64/default?h=openSUSE-15.2) 51 | - kernel config: within sources, see `config/x86_64/default` 52 | 53 | ## TinyCore 54 | 55 | Patched kernel sources, separate patches and config can be found among the 56 | [release sources](http://tinycorelinux.net/10.x/x86/release/src/kernel/). 57 | 58 | - [Core ISO](http://tinycorelinux.net/10.x/x86/release/Core-current.iso) 59 | - [CorePlus ISO](http://tinycorelinux.net/10.x/x86/release/CorePlus-current.iso) 60 | - [TinyCore ISO](http://tinycorelinux.net/10.x/x86/release/TinyCore-current.iso) 61 | - [kernel sources](http://tinycorelinux.net/10.x/x86/release/src/kernel/linux-4.19.10-patched.txz) 62 | - [kernel config](http://tinycorelinux.net/10.x/x86/release/src/kernel/config-4.19.10-tinycore) 63 | 64 | ## Ubuntu 65 | 66 | - [kernel sources](http://security.ubuntu.com/ubuntu/pool/main/l/linux-hwe/linux-source-5.0.0_5.0.0-37.40~18.04.1_all.deb) 67 | - kernel config: included in sources, split into parts for common, arch-specific 68 | and generic/low latency bits in `src/linux-source-5.0.0/debian.hwe/config/`: 69 | `config.common.ubuntu`, `amd64/config.{common.amd64,flavour.generic}` are 70 | the relevant files for webboot on x86 71 | -------------------------------------------------------------------------------- /docs/remaster-arch-iso.md: -------------------------------------------------------------------------------- 1 | # Remaster Arch Linux Live ISO for webboot 2 | 3 | ## Preparation 4 | 5 | In general, follow the steps as described in 6 | [the wiki](https://wiki.archlinux.org/index.php/Remastering_the_Install_ISO). 7 | 8 | For webboot, the following steps are necessary: 9 | 10 | - mount ISO 11 | - extract squashfs 12 | - copy over kernel 13 | - copy over [mkinitcpio.conf](https://git.archlinux.org/archiso.git/tree/configs/releng/mkinitcpio.conf) (overwrite `squashfs-root/etc/mkinitcpio.conf`) 14 | - chroot into rootfs 15 | - [build new initramfs](#building-the-new-initramfs-in-the-chroot) 16 | - copy mounted ISO 17 | - add the new initramfs to it (`iso/arch/boot/x86_64/archiso.img`) 18 | - build new ISO 19 | 20 | ## Building the new initramfs in the chroot 21 | 22 | Arch has a tool to build a suitable initramfs that is used to create the ISO 23 | images in the first place. So instead of extracting the existing initramfs, 24 | hacking on it, and rebuilding it, `mkinitcpio` will do it. There are two 25 | possible strategies: 26 | 27 | 1. add the pmem modules to the list of files to include (more generic) 28 | 2. write a custom hook to do it (https://github.com/archlinux/mkinitcpio/pull/30) 29 | 30 | ### Manual strategy 31 | 32 | #### Decompress the modules 33 | 34 | This is necessary because the initramfs cannot load compressed modules. 35 | 36 | ```sh 37 | cd /lib/modules/5.6.8-arch1-1/kernel/drivers/nvdimm/ 38 | unxz nd_btt.ko.xz 39 | unxz nd_e820.ko.xz 40 | unxz nd_pmem.ko.xz 41 | ``` 42 | 43 | #### Adjust the mkinitcpio config 44 | 45 | Add `FILES` to `/etc/mkinitcpio.conf`: 46 | 47 | ``` 48 | HOOKS=(base udev memdisk archiso_shutdown archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs archiso_kms block filesystems keyboard) 49 | COMPRESSION="xz" 50 | ### the above is from the copied releng/mkinitcpio.conf 51 | FILES=( 52 | /lib/modules/5.6.8-arch1-1/kernel/drivers/nvdimm/nd_btt.ko 53 | /lib/modules/5.6.8-arch1-1/kernel/drivers/nvdimm/nd_e820.ko 54 | /lib/modules/5.6.8-arch1-1/kernel/drivers/nvdimm/nd_pmem.ko) 55 | ``` 56 | 57 | ### Using a custom hook 58 | 59 | Create the file `/lib/initcpio/install/nvdimm`: 60 | 61 | ```sh 62 | #!/bin/bash 63 | 64 | build() { 65 | add_checked_modules '/drivers/nvdimm/' 66 | } 67 | ``` 68 | 69 | And add it to the `HOOKS` in `/etc/mkinitcpio.conf`: 70 | 71 | ``` 72 | HOOKS=(base udev memdisk archiso_shutdown archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs archiso_kms block filesystems keyboard nvdimm) 73 | COMPRESSION="xz" 74 | ``` 75 | 76 | ### Create the new initramfs 77 | 78 | ```sh 79 | _preset=`ls /etc/mkinitcpio.d/|sed 's#\..*##'` 80 | mkinitcpio -k $(ls /lib/modules/) -p $preset` 81 | ``` 82 | 83 | ## Try it out 84 | 85 | Check that everything works before rebuilding the ISO and trying to boot it. 86 | 87 | The location where you extracted the squashfs root may differ. This assumes 88 | following the guide, using the `run-webboot.sh` script from the repo: 89 | 90 | ```sh 91 | $ sh run-webboot.sh \ 92 | ~/customiso/arch/x86_64/squashfs-root/boot/vmlinuz-linux \ 93 | ~/customiso/arch/x86_64/squashfs-root/boot/initramfs.img 94 | ``` 95 | 96 | Check that the modules are present: 97 | 98 | ``` 99 | [rootfs ]# ls /lib/modules/5.6.8-arch1-1/kernel/drivers/nvdimm/ 100 | nd_btt.ko nd_e820.ko nd_pmem.ko 101 | ``` 102 | 103 | The modules should be loaded because `memmap` was passed from our script: 104 | 105 | ``` 106 | [rootfs ]# lsmod 107 | Module Size Used by 108 | nd_pmem 24576 0 109 | nd_btt 28672 1 nd_pmem 110 | serio_raw 20480 0 111 | atkbd 36864 0 112 | libps2 20480 1 atkbd 113 | nd_e820 16384 1 114 | sr_mod 28672 0 115 | cdrom 77824 1 sr_mod 116 | i8042 32768 0 117 | serio 28672 5 serio_raw,atkbd,i8042 118 | ``` 119 | 120 | And you should get a pmem device: 121 | 122 | ``` 123 | [rootfs ]# dmesg|grep pmem 124 | [ 9.013750] nd_pmem namespace0.0: unable to guarantee persistence of writes 125 | [ 9.172887] pmem0: detected capacity change from 0 to 805306368 126 | ``` 127 | 128 | If you get a confusing error in dmesg, try smaller memmap sizes. 129 | Some people wrote that steps of 64M or 256M should work. 130 | I had success with 768M, barely enough for the Arch ISO, and 4G, enough for 131 | bigger live environments including full graphical UIs such as Gnome or KDE. 132 | 133 | See also https://github.com/pmem/ndctl/issues/76#issuecomment-440849415. 134 | 135 | ## Rebuild the ISO 136 | 137 | Generally, follow the wiki. Mind the importance of the label of the ISO. 138 | We need to pass it when we kexec. The Arch ISO itself assumes booting 139 | through its own bootloader, and the hooks in its initramfs pick it up. 140 | In other words: Whatever label you choose, use it also in `webboot.go`. 141 | 142 | See the file `arch/boot/syslinux/archiso_sys.cfg` from the Arch ISO to see what 143 | the initramfs expects from the bootloader. 144 | -------------------------------------------------------------------------------- /docs/remaster-debian-iso.md: -------------------------------------------------------------------------------- 1 | # Remastering Debian live ISO 2 | 3 | ## Figure out kernel command line 4 | `less /mnt/iso/boot/grub/grub.cfg` 5 | 6 | ``` 7 | ... 8 | linux /live/vmlinuz-4.19.0-9-amd64 boot=live components splash quiet "${loopback}" 9 | initrd /live/initrd.img-4.19.0-9-amd64 10 | ... 11 | ``` 12 | 13 | ## Tailor initrd for webboot 14 | 15 | ### Extract initrd 16 | ```sh 17 | zcat /mnt/iso/live/initrd.img-4.19.0-9-amd64 | sudo cpio -idmv 18 | ``` 19 | 20 | ### Extract and copy nvdimm modules from squashfs 21 | ```sh 22 | unsquashfs -f /mnt/iso/live/filesystem.squashfs \ 23 | -e usr/lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm 24 | mv squashfs-root/usr/lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm \ 25 | lib/modules/4.19.0-9-amd64/kernel/drivers 26 | rm -r squashfs-root 27 | ``` 28 | 29 | ### Add insmod statements to /init - little hackaround :) 30 | ```sh 31 | insmod /lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm/libnvdimm.ko 32 | insmod /lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm/nd_btt.ko 33 | insmod /lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm/nd_pmem.ko 34 | insmod /lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm/nd_e820.ko 35 | insmod /lib/modules/4.19.0-9-amd64/kernel/drivers/nvdimm/nd_blk.ko 36 | ``` 37 | 38 | ### Rebuild initrd 39 | ```sh 40 | find . | cpio --create --format='newc' | gzip > initrd 41 | ``` 42 | 43 | ## Build new ISO 44 | 45 | ```sh 46 | cp -a /mnt/iso/ debian-remastered/ 47 | cp initrd.img-4.19.0-9-amd64 debian-remastered/live/initrd.img-4.19.0-9-amd64 48 | cd debian-remastered 49 | genisoimage \ 50 | -l -r -J -V "DEBIAN_WEBBOOT" \ 51 | -b isolinux/isolinux.bin \ 52 | -no-emul-boot -boot-load-size 4 -boot-info-table \ 53 | -c isolinux/boot.cat \ 54 | -o ../debian.iso \ 55 | ./ 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/remaster-manjaro-iso.md: -------------------------------------------------------------------------------- 1 | # Remastering a Manjaro ISO 2 | 3 | From the [Manjaro wiki](https://wiki.manjaro.org/index.php?title=Manjaro-tools#buildiso) 4 | it is unclear how the initramfs is compiled. 5 | 6 | ## Inspecting the ISO 7 | 8 | The structure differs from the Arch ISO. There are multiple 9 | squashfs files. 10 | 11 | ```sh 12 | $ ls -l /mnt/iso/manjaro/x86_64/ 13 | total 2.6G 14 | -r--r--r-- 1 root root 48 May 11 08:56 desktopfs.md5 15 | -r--r--r-- 1 root root 1.3G May 11 08:56 desktopfs.sfs 16 | -r--r--r-- 1 root root 45 May 11 09:40 livefs.md5 17 | -r--r--r-- 1 root root 61M May 11 09:40 livefs.sfs 18 | -r--r--r-- 1 root root 45 May 11 08:54 mhwdfs.md5 19 | -r--r--r-- 1 root root 618M May 11 08:54 mhwdfs.sfs 20 | -r--r--r-- 1 root root 45 May 11 08:58 rootfs.md5 21 | -r--r--r-- 1 root root 585M May 11 08:58 rootfs.sfs 22 | ``` 23 | 24 | Extracting `rootfs.sfs` and following the approach for Arch errors: 25 | 26 | ```sh 27 | $ mkinitcpio -k $(ls /lib/modules/) -p linux56 28 | ==> Building image from preset: /etc/mkinitcpio.d/linux56.preset: 'default' 29 | -> -k /boot/vmlinuz-5.6-x86_64 -c /etc/mkinitcpio.conf -g /boot/initramfs-5.6-x86_64.img 30 | ==> Starting build: 5.6.11-1-MANJARO 31 | -> Running build hook: [base] 32 | -> Running build hook: [udev] 33 | -> Running build hook: [memdisk] 34 | ==> ERROR: file not found: `memdiskfind' 35 | ==> ERROR: Hook 'archiso_shutdown' cannot be found 36 | ==> ERROR: Hook 'archiso' cannot be found 37 | ==> ERROR: Hook 'archiso_loop_mnt' cannot be found 38 | ==> ERROR: Hook 'archiso_pxe_common' cannot be found 39 | ==> ERROR: Hook 'archiso_pxe_nbd' cannot be found 40 | ==> ERROR: Hook 'archiso_pxe_http' cannot be found 41 | ==> ERROR: Hook 'archiso_pxe_nfs' cannot be found 42 | ==> ERROR: Hook 'archiso_kms' cannot be found 43 | -> Running build hook: [block] 44 | -> Running build hook: [filesystems] 45 | -> Running build hook: [keyboard] 46 | -> Running build hook: [nvdimm] 47 | ==> Generating module dependencies 48 | ==> Creating xz-compressed initcpio image: /boot/initramfs-5.6-x86_64.img 49 | ==> WARNING: errors were encountered during the build. The image may not be complete. 50 | ``` 51 | 52 | The hooks used for `archiso` are apparently not available. 53 | 54 | The `mkinitcpio.conf` from `desktopfs.sfs` has `systemd` and `ostree`: 55 | 56 | ``` 57 | HOOKS="base systemd ostree autodetect modconf block filesystems keyboard fsck" 58 | ``` 59 | 60 | Which leads to no success either: 61 | 62 | ```sh 63 | $ mkinitcpio -k 5.6.11-1-MANJARO -g /boot/initramfs.img 64 | ==> Starting build: 5.6.11-1-MANJARO 65 | -> Running build hook: [base] 66 | -> Running build hook: [systemd] 67 | ==> ERROR: Hook 'ostree' cannot be found 68 | -> Running build hook: [autodetect] 69 | ==> ERROR: failed to detect root filesystem 70 | -> Running build hook: [modconf] 71 | -> Running build hook: [block] 72 | -> Running build hook: [filesystems] 73 | -> Running build hook: [keyboard] 74 | -> Running build hook: [fsck] 75 | -> Running build hook: [nvdimm] 76 | ==> Generating module dependencies 77 | ==> Creating xz-compressed initcpio image: /boot/initramfs.img 78 | ==> WARNING: errors were encountered during the build. The image may not be complete. 79 | ``` 80 | 81 | ## Booting the ISO for more insight 82 | 83 | ```sh 84 | $ qemu-system-x86_64 -cdrom manjaro-gnome-20.0.1-200511-linux56.iso 85 | ``` 86 | 87 | ### Kernel arguments 88 | 89 | A look at `cat /proc/cmdline` yields the crucial bits to pass to the 90 | kernel so that the initramfs can pick it up to find the ISO on devices. 91 | The naming is different from Arch / SystemRescueCd: `misolabel` and 92 | `misobasedir`. 93 | 94 | ``` 95 | misobasedir=manjaro misolabel=MANJARO_GNOME_2001 96 | ``` 97 | 98 | ## Rebuilding the initramfs manually 99 | 100 | The steps are simple, just require some knowledge: 101 | 102 | - extract the initramfs 103 | - copy the modules into it, `unxz` them 104 | - recreate the initramfs 105 | 106 | Luckily, the [Red Hat docs](https://access.redhat.com/solutions/24029) 107 | explain how to recreate a `cpio` image, boiling down to the following: 108 | 109 | ```sh 110 | $ find . | cpio --create --format='newc' > ../new.img 111 | ``` 112 | 113 | The compressed image needs a special alignment. We have that covered in 114 | the [u-root README](https://github.com/u-root/u-root#compression): 115 | 116 | ```sh 117 | $ xz --check=crc32 -9 --lzma2=dict=1MiB \ 118 | --stdout /mnt/tmp/new.img \ 119 | | sudo dd conv=sync bs=512 \ 120 | of=initramfs-x86_64.img 121 | ``` 122 | 123 | Trying it out in QEMU though calls for trouble again: 124 | 125 | ``` 126 | $ qemu-system-x86_64 \ 127 | -machine q35,accel=kvm -m 1G -append 'memmap=512M!512M' \ 128 | -kernel manjaro-remastered/boot/vmlinuz-x86_64 \ 129 | -initrd manjaro-remastered/boot/initramfs-x86_64.img 130 | ``` 131 | 132 | There is no `/dev/pmem*` device. Uh-oh! And the modules were not loaded 133 | either - but why? An attempt to `modprobe nd_pmem` tells that the 134 | module cannot be found. But the file is present. A quick research says 135 | that the module lookup needs some help. The `depmod` utility will 136 | receate the `modules.dep` file in `/usr/lib/$KERNEL/`. 137 | Solution: simply add `depmod` to the top of `/init`. 138 | 139 | Recreating the initramfs now leads to an environment with the desired 140 | pmem device present. 141 | 142 | ## Recreating the ISO 143 | 144 | First, copy the new initramfs to a copy is the ISO in some directory. 145 | 146 | Long story short: It's not `syslinux` like Arch does it, life is short. 147 | The resulting command is: 148 | 149 | ```sh 150 | $ genisoimage \ 151 | -l -r -J -V "MANJARO_WEBBOOT" \ 152 | -b efi/boot/bootx64.efi \ 153 | -no-emul-boot -boot-load-size 4 -boot-info-table \ 154 | -c boot.catalog \ 155 | -o ../manjaro.iso \ 156 | ./ 157 | ``` 158 | 159 | This loses the ability to boot via QEMU directly, which is a shortcut 160 | here for the proof of concept. For a full solution, the modules would 161 | just be added to the initramfs by the upstream distribution anyway. 162 | 163 | Now `webboot` successfully boots Manjaro with a full Gnome dekstop. :) 164 | 165 | ## TODOs 166 | 167 | After some more search, here are the hooks used to create the Manjaro initramfs: 168 | https://gitlab.manjaro.org/tools/development-tools/manjaro-tools/-/tree/master/initcpio/hooks 169 | 170 | File a PR there to include another hook for adding the nvdimm/pmem modules. 171 | -------------------------------------------------------------------------------- /firsttime.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | sudo apt-get install build-essential kexec-tools libelf-dev libnl-3-dev libnl-genl-3-dev libssl-dev qemu-system-x86 wireless-tools wpasupplicant 5 | 6 | pwd 7 | ls 8 | git clone https://github.com/u-root/u-root.git ../u-root 9 | (cd ../u-root/ && go install .) 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/u-root/webboot 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gizak/termui/v3 v3.1.0 7 | github.com/nsf/termbox-go v1.0.0 8 | github.com/u-root/u-root v0.11.0 9 | github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 10 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 11 | golang.org/x/sys v0.4.0 12 | ) 13 | 14 | require ( 15 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 16 | github.com/dustin/go-humanize v1.0.1 // indirect 17 | github.com/golang/protobuf v1.3.3 // indirect 18 | github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f // indirect 19 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect 20 | github.com/hashicorp/errwrap v1.1.0 // indirect 21 | github.com/hashicorp/go-multierror v1.1.1 // indirect 22 | github.com/insomniacslk/dhcp v0.0.0-20211209223715-7d93572ebe8e // indirect 23 | github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531 // indirect 24 | github.com/klauspost/compress v1.10.6 // indirect 25 | github.com/mattn/go-runewidth v0.0.9 // indirect 26 | github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect 27 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect 28 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 29 | github.com/pierrec/lz4/v4 v4.1.17 // indirect 30 | github.com/rekby/gpt v0.0.0-20200614112001-7da10aec5566 // indirect 31 | github.com/spf13/pflag v1.0.5 // indirect 32 | github.com/stretchr/testify v1.7.3 // indirect 33 | github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 // indirect 34 | github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect 35 | github.com/ulikunitz/xz v0.5.8 // indirect 36 | github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect 37 | golang.org/x/mod v0.7.0 // indirect 38 | golang.org/x/net v0.5.0 // indirect 39 | golang.org/x/tools v0.5.0 // indirect 40 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 // indirect 41 | google.golang.org/grpc v1.31.0 // indirect 42 | pack.ag/tftp v1.0.1-0.20181129014014-07909dfbde3c // indirect 43 | ) 44 | -------------------------------------------------------------------------------- /integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Intended for CI but can be run standalone. 4 | # 5 | # Runs an integration test using the distro name passed as the first argument. 6 | # The list of accepted distros is in "integration/basic_test.go". 7 | # 8 | # Example usage: 9 | # cd /path/to/webboot/repo 10 | # sudo apt-get update 11 | # ./firsttime.sh 12 | # ./integration.sh TinyCore 13 | 14 | # Exit immediately if a command fails 15 | set -e 16 | 17 | if [[ $# -eq 0 ]] ; then 18 | echo "integration.sh: No argument supplied. Please specify a distro as the first argument." 1>&2 19 | exit 1 20 | fi 21 | 22 | ( 23 | cd integration 24 | 25 | # Download if bzImage not present 26 | wget --no-clobber https://github.com/u-root/webboot-distro/raw/master/CIkernels/5.6.14/bzImage 27 | 28 | # Print full path 29 | ls -dl "$PWD"/bzImage 30 | 31 | # Tests distro specified by first argument 32 | WEBBOOT_DISTRO="$1" \ 33 | UROOT_QEMU="qemu-system-x86_64" \ 34 | UROOT_KERNEL="$PWD/bzImage" \ 35 | UROOT_INITRAMFS="/tmp/initramfs.linux_amd64.cpio" \ 36 | go test -v -timeout 60m # Matches behavior of `vmtest.QEMUTest` in basic_test.go 37 | ) 38 | -------------------------------------------------------------------------------- /integration/basic_test.go: -------------------------------------------------------------------------------- 1 | // license that can be found in the LICENSE file. 2 | 3 | //go:build !race 4 | // +build !race 5 | 6 | package integration 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "testing" 13 | "time" 14 | 15 | "github.com/u-root/u-root/pkg/qemu" 16 | "github.com/u-root/u-root/pkg/vmtest" 17 | ) 18 | 19 | var expectString = map[string]string{ 20 | "Arch": "TODO_PLEASE_SET_EXPECT_STRING", 21 | "CentOS 7": "TODO_PLEASE_SET_EXPECT_STRING", 22 | "Debian": "TODO_PLEASE_SET_EXPECT_STRING", 23 | "Fedora": "Fedora-WS-Live-32-1-6", 24 | "Kali": "TODO_PLEASE_SET_EXPECT_STRING", 25 | "Linux Mint": "TODO_PLEASE_SET_EXPECT_STRING", 26 | "Manjaro": "TODO_PLEASE_SET_EXPECT_STRING", 27 | "TinyCore": "5.15.10-tinycore64", 28 | "Ubuntu": "TODO_PLEASE_SET_EXPECT_STRING", 29 | } 30 | 31 | func TestScript(t *testing.T) { 32 | // The vmtest packages do not work any more and I'm a bit tired 33 | // of trying to figure out why. Damn modules. 34 | 35 | if _, err := os.Stat("u-root"); err != nil { 36 | c := exec.Command("git", "clone", "--single-branch", "https://github.com/u-root/u-root") 37 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 38 | if err := c.Run(); err != nil { 39 | t.Fatalf("cloning u-root: %v", err) 40 | } 41 | c = exec.Command("go", "build", ".") 42 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 43 | c.Dir = "u-root" 44 | if err := c.Run(); err != nil { 45 | t.Fatalf("cloning u-root: %v", err) 46 | } 47 | } 48 | 49 | var fail bool 50 | 51 | k, err := exec.LookPath("kexec") 52 | if err != nil { 53 | t.Fatalf("exec.LookPath(\"kexec\"): %v != nil", err) 54 | } 55 | 56 | webbootDistro := os.Getenv("WEBBOOT_DISTRO") 57 | if _, ok := expectString[webbootDistro]; !ok { 58 | fail = true 59 | if webbootDistro == "" { 60 | t.Errorf("WEBBOOT_DISTRO is not set") 61 | } 62 | t.Errorf("Unknown distro: %q", webbootDistro) 63 | } 64 | if _, ok := os.LookupEnv("UROOT_INITRAMFS"); !ok { 65 | fail = true 66 | t.Errorf("UROOT_INITRAMFS needs to be set") 67 | } 68 | if fail { 69 | t.Fatalf("can not continue due to errors") 70 | } 71 | 72 | c := exec.Command("./u-root/u-root", 73 | "-files", "../cmds/cli/ci.json:ci.json", 74 | "-files", k+":sbin/kexec", 75 | // /etc/ssl/certs contains symlinks to the certificate files in 76 | // /usr/share/certificates, so both are required 77 | "-files", "/etc/ssl/certs", 78 | "-files", "/usr/share/ca-certificates", 79 | 80 | "-uinitcmd=uinit", 81 | "../cmds/webboot", 82 | "../cmds/cli", 83 | 84 | "./u-root/integration/testcmd/generic/uinit", 85 | "./u-root/cmds/core/init", 86 | "./u-root/cmds/core/ip", 87 | "./u-root/cmds/core/shutdown", 88 | "./u-root/cmds/core/sleep", 89 | "./u-root/cmds/core/dhclient", 90 | "./u-root/cmds/core/elvish", 91 | "./u-root/cmds/boot/pxeboot") 92 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 93 | c.Env = append(os.Environ(), "GOARCH=amd64", "GOOS=linux") 94 | t.Logf("Args %v cmd %v", c.Args, c) 95 | if err := c.Run(); err != nil { 96 | t.Fatalf("Running u-root: %v", err) 97 | } 98 | 99 | // Host machine should have at least 4 GB of RAM to comfortably download an 100 | // ISO, which can be large 101 | q, cleanup := vmtest.QEMUTest(t, &vmtest.Options{ 102 | Name: "ShellScript", 103 | /* it would be so nice if this actually worked. 104 | BuildOpts: uroot.Opts{ 105 | Commands: uroot.BusyBoxCmds( 106 | "github.com/u-root/u-root/cmds/core/init", 107 | "github.com/u-root/u-root/cmds/core/ip", 108 | "github.com/u-root/u-root/cmds/core/shutdown", 109 | "github.com/u-root/u-root/cmds/core/sleep", 110 | "github.com/u-root/u-root/cmds/boot/pxeboot", 111 | "github.com/u-root/webboot/cmds/webboot", 112 | "github.com/u-root/webboot/cmds/cli", 113 | "github.com/u-root/u-root/cmds/core/dhclient", 114 | "github.com/u-root/u-root/cmds/core/elvish", 115 | ), 116 | ExtraFiles: []string{ 117 | "../cmds/cli/ci.json:ci.json", 118 | "/sbin/kexec", 119 | "/etc/ssl/certs", 120 | }, 121 | }, 122 | */ 123 | QEMUOpts: qemu.Options{ 124 | // Downloading an ISO may take a while 125 | Timeout: 60 * time.Minute, 126 | Devices: []qemu.Device{ 127 | qemu.ArbitraryArgs{ 128 | "-machine", "q35", 129 | "-device", "rtl8139,netdev=u1", 130 | "-netdev", "user,id=u1", 131 | "-m", "4G", 132 | }, 133 | }, 134 | KernelArgs: "UROOT_NOHWRNG=1", 135 | }, 136 | TestCmds: []string{ 137 | "echo HIHIHIHIHIHIHIHIHIHIHIHIHIHIHIHIHI", 138 | "dhclient -ipv6=f -v eth0", 139 | // The webbootDistro may contain spaces. 140 | // `cli` is a webboot command, see cmds/cli 141 | fmt.Sprintf("cli -verbose -distroName=%q", webbootDistro), 142 | "shutdown -h", 143 | }, 144 | }) 145 | defer cleanup() 146 | 147 | if err := q.Expect(expectString[webbootDistro]); err != nil { 148 | t.Fatalf("expected %q, got error: %v", expectString[webbootDistro], err) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /makeusb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | go run . 4 | mkdir -p /mnt/usb 5 | sudo mount /dev/$1 /mnt/usb 6 | gzip -f /tmp/initramfs.linux_amd64.cpio 7 | sudo cp /tmp/initramfs.linux_amd64.cpio.gz /mnt/usb/boot/webboot.cpio.gz 8 | sudo umount /mnt/usb 9 | 10 | -------------------------------------------------------------------------------- /pkg/bootiso/bootiso.go: -------------------------------------------------------------------------------- 1 | package bootiso 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "hash" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "os" 16 | "os/exec" 17 | "path" 18 | "runtime/debug" 19 | "strings" 20 | 21 | "github.com/u-root/u-root/pkg/boot" 22 | "github.com/u-root/u-root/pkg/boot/grub" 23 | "github.com/u-root/u-root/pkg/boot/kexec" 24 | "github.com/u-root/u-root/pkg/boot/syslinux" 25 | "github.com/u-root/u-root/pkg/boot/util" 26 | "github.com/u-root/u-root/pkg/mount" 27 | "github.com/u-root/u-root/pkg/mount/block" 28 | "github.com/u-root/u-root/pkg/mount/loop" 29 | "github.com/u-root/u-root/pkg/uio" 30 | "golang.org/x/sys/unix" 31 | ) 32 | 33 | type Config struct { 34 | Label string 35 | KernelPath string 36 | InitrdPath string 37 | Cmdline string 38 | } 39 | 40 | // ParseConfigFromISO mounts the iso file, attempts to parse the config file, 41 | // and returns a list of bootable boot.OSImage objects representing the parsed configs 42 | func ParseConfigFromISO(isoPath string, configType string) ([]boot.OSImage, error) { 43 | tmp, err := ioutil.TempDir("", "mnt-") 44 | if err != nil { 45 | return nil, fmt.Errorf("Error creating mount dir: %v", err) 46 | } 47 | defer os.RemoveAll(tmp) 48 | 49 | loopdev, err := loop.New(isoPath, "iso9660", "") 50 | if err != nil { 51 | return nil, fmt.Errorf("Error creating loop device: %v", err) 52 | } 53 | 54 | mp, err := loopdev.Mount(tmp, unix.MS_RDONLY|unix.MS_NOATIME) 55 | if err != nil { 56 | return nil, fmt.Errorf("Error mounting loop device: %v", err) 57 | } 58 | defer mp.Unmount(0) 59 | 60 | images, err := parseConfigFile(tmp, configType) 61 | if err != nil { 62 | return nil, fmt.Errorf("Error parsing config: %v", err) 63 | } 64 | 65 | return images, nil 66 | } 67 | 68 | // LoadCustomConfigs is an alternative to ParseConfigFromISO that allows us 69 | // to define the boot parameters ourselves (in a list of Config objects) 70 | // instead of parsing them from a config file 71 | func LoadCustomConfigs(isoPath string, configs []Config) ([]boot.OSImage, error) { 72 | tmpDir, err := ioutil.TempDir("", "mnt-") 73 | if err != nil { 74 | return nil, fmt.Errorf("Error on ioutil.TempDir; in %s, and got %v", debug.Stack(), err) 75 | } 76 | 77 | loopdev, err := loop.New(isoPath, "iso9660", "") 78 | if err != nil { 79 | return nil, fmt.Errorf("Error on loop.New; in %s, and got %v", debug.Stack(), err) 80 | } 81 | 82 | mp, err := loopdev.Mount(tmpDir, unix.MS_RDONLY|unix.MS_NOATIME) 83 | if err != nil { 84 | return nil, fmt.Errorf("Error on loopdev.Mount; in %s, and got %v", debug.Stack(), err) 85 | } 86 | 87 | var images []boot.OSImage 88 | var files []*os.File 89 | copied := make(map[string]*os.File) 90 | 91 | defer func() { 92 | for _, f := range files { 93 | if err = f.Close(); err != nil { 94 | log.Print(err) 95 | } 96 | } 97 | 98 | if err = mp.Unmount(unix.MNT_FORCE); err != nil { 99 | log.Fatal(err) 100 | } 101 | 102 | // Use Remove rather than RemoveAll to avoid 103 | // removal if the directory is not empty 104 | if err = os.Remove(tmpDir); err != nil { 105 | log.Fatal(err) 106 | } 107 | }() 108 | 109 | for _, c := range configs { 110 | var tmpKernel, tmpInitrd *os.File 111 | 112 | // Copy kernel to temp if we haven't already 113 | if _, ok := copied[c.KernelPath]; !ok { 114 | kernel, err := os.Open(path.Join(tmpDir, c.KernelPath)) 115 | if err != nil { 116 | return nil, fmt.Errorf("Error on os.Open; in %s, and got %v", debug.Stack(), err) 117 | } 118 | files = append(files, kernel) 119 | 120 | // Temp files are not added to the files list 121 | // since they need to stay open for later reading 122 | tmpKernel, err = ioutil.TempFile("", "kernel-") 123 | if err != nil { 124 | return nil, fmt.Errorf("Error on ioutil.TempFile; in %s, and got %v", debug.Stack(), err) 125 | } 126 | 127 | if _, err = io.Copy(tmpKernel, kernel); err != nil { 128 | return nil, fmt.Errorf("Error on io.Copy; in %s, and got %v", debug.Stack(), err) 129 | } 130 | 131 | if _, err = tmpKernel.Seek(0, 0); err != nil { 132 | return nil, fmt.Errorf("Error on tmpKernel.Seek; in %s, and got %v", debug.Stack(), err) 133 | } 134 | 135 | copied[c.KernelPath] = tmpKernel 136 | } else { 137 | tmpKernel = copied[c.KernelPath] 138 | } 139 | 140 | // Copy initrd to temp if we haven't already 141 | if _, ok := copied[c.InitrdPath]; !ok { 142 | initrd, err := os.Open(path.Join(tmpDir, c.InitrdPath)) 143 | if err != nil { 144 | return nil, fmt.Errorf("Error on os.Open; in %s, and got %v", debug.Stack(), err) 145 | } 146 | files = append(files, initrd) 147 | 148 | tmpInitrd, err = ioutil.TempFile("", "initrd-") 149 | if err != nil { 150 | return nil, fmt.Errorf("Error on ioutil.TempFile; in %s, and got %v", debug.Stack(), err) 151 | } 152 | 153 | if _, err = io.Copy(tmpInitrd, initrd); err != nil { 154 | return nil, fmt.Errorf("Error on io.Copy; in %s, and got %v", debug.Stack(), err) 155 | } 156 | 157 | if _, err = tmpInitrd.Seek(0, 0); err != nil { 158 | return nil, fmt.Errorf("Error on tmpInitrd.Seek; in %s, and got %v", debug.Stack(), err) 159 | } 160 | 161 | copied[c.InitrdPath] = tmpInitrd 162 | } else { 163 | tmpInitrd = copied[c.InitrdPath] 164 | } 165 | 166 | images = append(images, &boot.LinuxImage{ 167 | Name: c.Label, 168 | Kernel: tmpKernel, 169 | Initrd: tmpInitrd, 170 | Cmdline: c.Cmdline, 171 | }) 172 | } 173 | 174 | return images, nil 175 | } 176 | 177 | // BootFromPmem copies the ISO to pmem0 and boots 178 | // given the syslinux configuration with the provided label 179 | func BootFromPmem(isoPath string, configLabel string, configType string) error { 180 | pmem, err := os.OpenFile("/dev/pmem0", os.O_APPEND|os.O_WRONLY, 0600) 181 | if err != nil { 182 | return fmt.Errorf("Error opening persistent memory device: %v", err) 183 | } 184 | 185 | iso, err := os.Open(isoPath) 186 | if err != nil { 187 | return fmt.Errorf("Error opening ISO: %v", err) 188 | } 189 | defer iso.Close() 190 | 191 | if _, err := io.Copy(pmem, iso); err != nil { 192 | return fmt.Errorf("Error copying from ISO to pmem: %v", err) 193 | } 194 | if err = pmem.Close(); err != nil { 195 | return fmt.Errorf("Error closing persistent memory device: %v", err) 196 | } 197 | 198 | tmp, err := ioutil.TempDir("", "mnt") 199 | if err != nil { 200 | return fmt.Errorf("Error creating temp directory: %v", err) 201 | } 202 | defer os.RemoveAll(tmp) 203 | 204 | if _, err := mount.Mount("/dev/pmem0", tmp, "iso9660", "", unix.MS_RDONLY|unix.MS_NOATIME); err != nil { 205 | return fmt.Errorf("Error mounting pmem0 to temp directory: %v", err) 206 | } 207 | 208 | configOpts, err := parseConfigFile(tmp, configType) 209 | if err != nil { 210 | return fmt.Errorf("Error retrieving syslinux config options: %v", err) 211 | } 212 | 213 | osImage := findConfigOptionByLabel(configOpts, configLabel) 214 | if osImage == nil { 215 | return fmt.Errorf("Config option with the requested label does not exist") 216 | } 217 | 218 | // Need to convert from boot.OSImage to boot.LinuxImage to edit the Cmdline 219 | linuxImage, ok := osImage.(*boot.LinuxImage) 220 | if !ok { 221 | return fmt.Errorf("Error converting from boot.OSImage to boot.LinuxImage") 222 | } 223 | 224 | localCmd, err := ioutil.ReadFile("/proc/cmdline") 225 | if err != nil { 226 | return fmt.Errorf("Error accessing /proc/cmdline") 227 | } 228 | cmdline := strings.TrimSuffix(string(localCmd), "\n") + " " + linuxImage.Cmdline 229 | linuxImage.Cmdline = cmdline 230 | 231 | if err := linuxImage.Load(true); err != nil { 232 | return err 233 | } 234 | if err := kexec.Reboot(); err != nil { 235 | return err 236 | } 237 | 238 | return nil 239 | } 240 | 241 | // next two functions hoisted from u-root kexec. We will remove 242 | // them when the u-root kexec becomes capable of using the 32-bit 243 | // entry point. 32-bit entry is essential to working on chromebooks. 244 | 245 | func copyToFile(r io.Reader) (*os.File, error) { 246 | f, err := ioutil.TempFile("", "webboot") 247 | if err != nil { 248 | return nil, fmt.Errorf("Error on ioutil.TempFile; in %s, and got %v", debug.Stack(), err) 249 | } 250 | defer f.Close() 251 | if _, err := io.Copy(f, r); err != nil { 252 | return nil, fmt.Errorf("Error on io.Copy; in %s, and got %v", debug.Stack(), err) 253 | } 254 | if err := f.Sync(); err != nil { 255 | return nil, fmt.Errorf("Error on f.Sync; in %s, and got %v", debug.Stack(), err) 256 | } 257 | 258 | readOnlyF, err := os.Open(f.Name()) 259 | if err != nil { 260 | return nil, fmt.Errorf("Error on os.Open; in %s, and got %v", debug.Stack(), err) 261 | } 262 | return readOnlyF, nil 263 | } 264 | 265 | // kexecCmd boots via the classic kexec command, if it exists 266 | func cmdKexecLoad(li *boot.LinuxImage, verbose bool) error { 267 | if li.Kernel == nil { 268 | return errors.New("LinuxImage.Kernel must be non-nil") 269 | } 270 | 271 | kernel, initrd := uio.Reader(util.TryGzipFilter(li.Kernel)), uio.Reader(li.Initrd) 272 | if verbose { 273 | // In verbose mode, print a dot every 5MiB. It is not pretty, 274 | // but it at least proves the files are still downloading. 275 | progress := func(r io.Reader, dot string) io.Reader { 276 | return &uio.ProgressReadCloser{ 277 | RC: ioutil.NopCloser(r), 278 | Symbol: dot, 279 | Interval: 5 * 1024 * 1024, 280 | W: os.Stdout, 281 | } 282 | } 283 | kernel = progress(kernel, "K") 284 | initrd = progress(initrd, "I") 285 | } 286 | 287 | // It seams inefficient to always copy, in particular when the reader 288 | // is an io.File but that's not sufficient, os.File could be a socket, 289 | // a pipe or some other strange thing. Also kexec_file_load will fail 290 | // (similar to execve) if anything as the file opened for writing. 291 | // That's unfortunately something we can't guarantee here - unless we 292 | // make a copy of the file and dump it somewhere. 293 | k, err := copyToFile(kernel) 294 | if err != nil { 295 | return err 296 | } 297 | defer k.Close() 298 | kargs := []string{"-d", "-l", "--entry-32bit", "--command-line=" + li.Cmdline} 299 | var i *os.File 300 | if li.Initrd != nil { 301 | i, err = copyToFile(initrd) 302 | if err != nil { 303 | return err 304 | } 305 | defer i.Close() 306 | kargs = append(kargs, "--initrd="+i.Name()) 307 | } 308 | 309 | log.Printf("Kernel: %s", k.Name()) 310 | kargs = append(kargs, k.Name()) 311 | if i != nil { 312 | log.Printf("Initrd: %s", i.Name()) 313 | } 314 | log.Printf("Command line: %s", li.Cmdline) 315 | log.Printf("Kexec args: %q", kargs) 316 | 317 | out, err := exec.Command("/sbin/kexec", kargs...).CombinedOutput() 318 | if err != nil { 319 | err = fmt.Errorf("Load failed; output %q, err %v", out, err) 320 | } 321 | return err 322 | } 323 | 324 | func cmdKexecReboot(verbose bool) error { 325 | o, err := exec.Command("/sbin/kexec", "-d", "-e").CombinedOutput() 326 | if err != nil { 327 | err = fmt.Errorf("Exec failed; output %q, err %v", o, err) 328 | } 329 | return err 330 | } 331 | 332 | func BootCachedISO(osImage boot.OSImage, kernelParams string) error { 333 | // Need to convert from boot.OSImage to boot.LinuxImage to edit the Cmdline 334 | linuxImage, ok := osImage.(*boot.LinuxImage) 335 | if !ok { 336 | return fmt.Errorf("Error converting from boot.OSImage to boot.LinuxImage") 337 | } 338 | 339 | linuxImage.Cmdline = linuxImage.Cmdline + " " + kernelParams 340 | 341 | // We prefer to use the kexec command for now, if possible, as it can 342 | // use the 32-bit entry point. 343 | if _, err := os.Stat("/sbin/kexec"); err != nil { 344 | if err := cmdKexecLoad(linuxImage, true); err != nil { 345 | return err 346 | } 347 | if err := cmdKexecReboot(true); err != nil { 348 | return err 349 | } 350 | } 351 | if err := linuxImage.Load(true); err != nil { 352 | return err 353 | } 354 | 355 | if err := kexec.Reboot(); err != nil { 356 | return err 357 | } 358 | 359 | return nil 360 | } 361 | 362 | // VerifyChecksum takes a path to the ISO and its checksum 363 | // and compares the calculated checksum on the ISO against the checksum. 364 | // It returns true if the checksum was correct, false if the checksum 365 | // was incorrect, the calculated checksum, and an error. 366 | func VerifyChecksum(isoPath, checksum, checksumType string) (bool, string, error) { 367 | iso, err := os.Open(isoPath) 368 | if err != nil { 369 | return false, "", err 370 | } 371 | defer iso.Close() 372 | 373 | var hash hash.Hash 374 | switch checksumType { 375 | case "md5": 376 | hash = md5.New() 377 | case "sha1": 378 | hash = sha1.New() 379 | case "sha256": 380 | hash = sha256.New() 381 | default: 382 | return false, "", fmt.Errorf("Unknown checksum type.") 383 | } 384 | 385 | if _, err := io.Copy(hash, iso); err != nil { 386 | return false, "", err 387 | } 388 | calcChecksum := hex.EncodeToString(hash.Sum(nil)) 389 | 390 | return calcChecksum == checksum, calcChecksum, nil 391 | } 392 | 393 | func findConfigOptionByLabel(configOptions []boot.OSImage, configLabel string) boot.OSImage { 394 | for _, config := range configOptions { 395 | if config.Label() == configLabel { 396 | return config 397 | } 398 | } 399 | return nil 400 | } 401 | 402 | func parseConfigFile(mountDir string, configType string) ([]boot.OSImage, error) { 403 | devs, err := block.GetBlockDevices() 404 | if err != nil { 405 | return nil, fmt.Errorf("Error on block.GetBlockDevices; in %s, and got %v", debug.Stack(), err) 406 | } 407 | mp := &mount.Pool{} 408 | 409 | if configType == "syslinux" { 410 | return syslinux.ParseLocalConfig(context.Background(), mountDir) 411 | } else if configType == "grub" { 412 | return grub.ParseLocalConfig(context.Background(), mountDir, devs, mp) 413 | } 414 | 415 | // If no config type was specified, try both grub and syslinux 416 | configOpts, err := syslinux.ParseLocalConfig(context.Background(), mountDir) 417 | if err == nil && len(configOpts) != 0 { 418 | return configOpts, err 419 | } 420 | return grub.ParseLocalConfig(context.Background(), mountDir, devs, mp) 421 | } 422 | -------------------------------------------------------------------------------- /pkg/bootiso/bootiso_test.go: -------------------------------------------------------------------------------- 1 | package bootiso 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var isoPath string = "testdata/TinyCorePure64.iso" 11 | 12 | func TestParseConfigFromISO(t *testing.T) { 13 | configOpts, err := ParseConfigFromISO(isoPath, "syslinux") 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | 18 | expectedLabels := [4]string{ 19 | "Boot TinyCorePure64", 20 | "Boot TinyCorePure64 (on slow devices, waitusb=5)", 21 | "Boot Core (command line only).", 22 | "Boot Core (command line only on slow devices, waitusb=5)", 23 | } 24 | 25 | for i, config := range configOpts { 26 | if config.Label() != expectedLabels[i] { 27 | t.Error("Invalid configuration option found.") 28 | } 29 | } 30 | } 31 | 32 | func TestChecksum(t *testing.T) { 33 | for _, test := range []struct { 34 | name string 35 | checksum string 36 | checksumType string 37 | valid bool 38 | }{ 39 | { 40 | name: "valid_md5", 41 | checksum: "10a79ba7558598574cd396e7b1b057b7", 42 | checksumType: "md5", 43 | valid: true, 44 | }, 45 | { 46 | name: "valid_sha256", 47 | checksum: "01ce6b5f4e4f7e98eddc343fc14f1436fb1b0452e6b9f7e07461b6a089a909c1", 48 | checksumType: "sha256", 49 | valid: true, 50 | }, 51 | { 52 | name: "invalid_md5", 53 | checksum: "99979ba7558598574cd396e7b1b057b7", 54 | checksumType: "md5", 55 | valid: false, 56 | }, 57 | } { 58 | t.Run(test.name, func(t *testing.T) { 59 | valid, calcChecksum, err := VerifyChecksum(isoPath, test.checksum, test.checksumType) 60 | if err != nil { 61 | t.Error(err) 62 | } else if valid != test.valid { 63 | t.Errorf("Checksum validation was expected to result in %t.\n", test.valid) 64 | } else if len(calcChecksum) == 0 { 65 | t.Errorf("Should have returned a checksum") 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestCustomConfigs(t *testing.T) { 72 | var configs []Config 73 | for i := 0; i < 5; i++ { 74 | configs = append(configs, Config{ 75 | Label: "Custom Config " + fmt.Sprint(i), 76 | KernelPath: "/boot/vmlinuz64", 77 | InitrdPath: "/boot/corepure64.gz", 78 | Cmdline: "loglevel=3 vga=791", 79 | }) 80 | } 81 | 82 | for _, test := range []struct { 83 | name string 84 | configs []Config 85 | }{ 86 | { 87 | name: "empty_list", 88 | configs: []Config{}, 89 | }, 90 | { 91 | name: "single_config", 92 | configs: configs[:1], 93 | }, 94 | { 95 | name: "multiple_configs", 96 | configs: configs, 97 | }, 98 | } { 99 | t.Run(test.name, func(t *testing.T) { 100 | images, err := LoadCustomConfigs(isoPath, test.configs) 101 | if err != nil { 102 | t.Error(err) 103 | } else if len(test.configs) != len(images) { 104 | t.Errorf("Test contained %d configs, but only received %d images.", len(test.configs), len(images)) 105 | } 106 | 107 | for index, image := range images { 108 | if test.configs[index].Label != image.Label() { 109 | t.Errorf("Expected label %q but received %q.", test.configs[index].Label, image.Label()) 110 | } 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func TestMain(m *testing.M) { 117 | if _, err := os.Stat(isoPath); err != nil { 118 | log.Fatal("ISO file was not found in the testdata directory.") 119 | } 120 | 121 | os.Exit(m.Run()) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/bootiso/testdata/TinyCorePure64.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-root/webboot/60bfe13edb27b6e90e4a060a4d1c30fe12307a6c/pkg/bootiso/testdata/TinyCorePure64.iso -------------------------------------------------------------------------------- /pkg/bootiso/testdata/TinyCorePure64.md5.txt: -------------------------------------------------------------------------------- 1 | 10a79ba7558598574cd396e7b1b057b7 TinyCorePure64.iso 2 | -------------------------------------------------------------------------------- /pkg/bootiso/testdata/TinyCorePure64.sha256.txt: -------------------------------------------------------------------------------- 1 | 01ce6b5f4e4f7e98eddc343fc14f1436fb1b0452e6b9f7e07461b6a089a909c1 TinyCorePure64.iso 2 | -------------------------------------------------------------------------------- /pkg/dhclient/dhclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package dhclient 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "regexp" 11 | "time" 12 | 13 | "github.com/u-root/u-root/pkg/dhclient" 14 | "github.com/vishvananda/netlink" 15 | ) 16 | 17 | // Request sets up the dhcp configurations for all of the ifNames. 18 | func Request(ifName string, timeout int, retry int, verbose bool, ipv4 bool, ipv6 bool, cl chan string) { 19 | ifRE := regexp.MustCompilePOSIX(ifName) 20 | 21 | ifnames, err := netlink.LinkList() 22 | if err != nil { 23 | cl <- fmt.Sprintf("Can't get list of link names: %v", err) 24 | close(cl) 25 | return 26 | } 27 | 28 | var filteredIfs []netlink.Link 29 | for _, iface := range ifnames { 30 | if ifRE.MatchString(iface.Attrs().Name) { 31 | filteredIfs = append(filteredIfs, iface) 32 | } 33 | } 34 | 35 | if len(filteredIfs) == 0 { 36 | cl <- fmt.Sprintf("No interfaces match %s", ifName) 37 | close(cl) 38 | return 39 | } 40 | 41 | go configureAll(filteredIfs, cl, timeout, retry, verbose, ipv4, ipv6) 42 | 43 | } 44 | 45 | func configureAll(ifs []netlink.Link, cl chan<- string, timeout int, retry int, verbose bool, ipv4 bool, ipv6 bool) { 46 | packetTimeout := time.Duration(timeout) * time.Second 47 | 48 | ctx, cancel := context.WithTimeout(context.Background(), packetTimeout*time.Duration(1< b { 38 | return a 39 | } 40 | return b 41 | } 42 | 43 | func countNewlines(str string) int { 44 | count := 0 45 | for _, s := range str { 46 | if s == '\n' { 47 | count++ 48 | } 49 | } 50 | return count 51 | } 52 | 53 | func Init() error { 54 | return ui.Init() 55 | } 56 | 57 | func Close() { 58 | ui.Close() 59 | } 60 | 61 | var BackRequest = errors.New("User requested to return to a previous menu.") 62 | var ExitRequest = errors.New("User requested to exit the program.") 63 | 64 | // AlwaysValid is a special isValid function that check nothing 65 | func AlwaysValid(input string) (string, string, bool) { 66 | return input, "", true 67 | } 68 | 69 | // newParagraph returns a widgets.Paragraph struct with given initial text. 70 | func newParagraph(initText string, border bool, location int, wid int, ht int) *widgets.Paragraph { 71 | p := widgets.NewParagraph() 72 | p.Text = initText 73 | p.Border = border 74 | p.SetRect(0, location, wid, location+ht) 75 | p.TextStyle.Fg = ui.ColorWhite 76 | return p 77 | } 78 | 79 | // readKey reads a key from input stream. 80 | func readKey(uiEvents <-chan ui.Event) string { 81 | for { 82 | e := <-uiEvents 83 | if e.Type == ui.KeyboardEvent || e.Type == ui.MouseEvent { 84 | return e.ID 85 | } 86 | } 87 | } 88 | 89 | // processInput presents an input box to user and returns the user's input. 90 | // processInput will check validation of input using isValid function. 91 | func processInput(introwords string, location int, wid int, ht int, isValid validCheck, uiEvents <-chan ui.Event) (string, string, error) { 92 | intro := newParagraph(introwords, false, location, len(introwords)+4, 3) 93 | location += 2 94 | input := newParagraph("", true, location, wid, ht+2) 95 | location += ht + 2 96 | warning := newParagraph(" to go back, to exit", false, location, wid, 15) 97 | 98 | ui.Render(intro) 99 | ui.Render(input) 100 | ui.Render(warning) 101 | 102 | // The input box is wid characters wide 103 | // - 2 chars are reserved for the left and right borders 104 | // - 1 char is left empty at the end of input to visually 105 | // signify that the text box is still accepting input 106 | // The user might want to input a string longer than wid-3 107 | // characters, so we store the full typed input in fullText 108 | // and display a substring of the full text to the user 109 | var fullText string 110 | 111 | for { 112 | k := readKey(uiEvents) 113 | switch k { 114 | case "": 115 | return "", "", ExitRequest 116 | case "": 117 | return "", "", BackRequest 118 | case "": 119 | inputString, warningString, ok := isValid(fullText) 120 | if ok { 121 | return inputString, warning.Text, nil 122 | } 123 | fullText = "" 124 | input.Text = "" 125 | warning.Text = warningString 126 | ui.Render(input) 127 | ui.Render(warning) 128 | case "": 129 | if len(input.Text) > 0 { 130 | fullText = fullText[:len(fullText)-1] 131 | start := max(0, len(fullText)-wid+3) 132 | input.Text = fullText[start:] 133 | ui.Render(input) 134 | } 135 | case "": 136 | fullText += " " 137 | start := max(0, len(fullText)-wid+3) 138 | input.Text = fullText[start:] 139 | ui.Render(input) 140 | default: 141 | // the termui use a string begin at '<' to represent some special keys 142 | // for example the 'F1' key will be parsed to "" string . 143 | // we should do nothing when meet these special keys, we only care about alphabets and digits. 144 | if k[0:1] != "<" { 145 | fullText += k 146 | start := max(0, len(fullText)-wid+3) 147 | input.Text = fullText[start:] 148 | ui.Render(input) 149 | } 150 | } 151 | } 152 | } 153 | 154 | // PromptTextInput opens a new input window with fixed width=100, hight=1. 155 | func PromptTextInput(introwords string, isValid validCheck, uiEvents <-chan ui.Event, menus chan<- string) (string, error) { 156 | menus <- introwords 157 | defer ui.Clear() 158 | input, _, err := processInput(introwords, 0, 80, 1, isValid, uiEvents) 159 | return input, err 160 | } 161 | 162 | // DisplayResult opens a new window and displays a message. 163 | // each item in the message array will be displayed on a single line. 164 | func DisplayResult(message []string, uiEvents <-chan ui.Event, menus chan<- string) (string, error) { 165 | menus <- message[0] 166 | 167 | defer ui.Clear() 168 | 169 | // if a message is longer then width of the window, split it to shorter lines 170 | var wid int = resultWidth 171 | text := []string{} 172 | for _, m := range message { 173 | for len(m) > wid { 174 | text = append(text, m[0:wid]) 175 | m = m[wid:] 176 | } 177 | text = append(text, m) 178 | } 179 | 180 | p := widgets.NewParagraph() 181 | p.Border = true 182 | p.SetRect(0, 0, resultWidth+2, resultHeight+4) 183 | p.TextStyle.Fg = ui.ColorWhite 184 | 185 | msgLength := len(text) 186 | first := 0 187 | last := min(resultHeight, msgLength) 188 | 189 | controlText := ", to scroll\n\nPress any other key to continue." 190 | controls := newParagraph(controlText, false, resultHeight+4, wid+2, 5) 191 | ui.Render(controls) 192 | 193 | for { 194 | p.Title = fmt.Sprintf("Message---%v/%v", first, msgLength) 195 | displayText := strings.Join(text[first:last], "\n") 196 | 197 | // Indicate whether user is at the 198 | // end of text for long messages 199 | if msgLength > resultHeight { 200 | if last < msgLength { 201 | displayText += "\n\n(More)" 202 | } else if last == msgLength { 203 | displayText += "\n\n(End of message)" 204 | } 205 | } 206 | 207 | p.Text = displayText 208 | ui.Render(p) 209 | 210 | k := readKey(uiEvents) 211 | switch k { 212 | case "", "": 213 | first = max(0, first-1) 214 | last = min(first+resultHeight, len(text)) 215 | case "", "": 216 | last = min(last+1, len(text)) 217 | first = max(0, last-resultHeight) 218 | case "", "": 219 | first = max(0, first-resultHeight) 220 | last = min(first+resultHeight, len(text)) 221 | case "", "": 222 | last = min(last+resultHeight, len(text)) 223 | first = max(0, last-resultHeight) 224 | case "": 225 | return p.Text, ExitRequest 226 | case "": 227 | return p.Text, BackRequest 228 | default: 229 | return p.Text, nil 230 | } 231 | } 232 | } 233 | 234 | // parsingMenuOption parses the user's operation in the menu page, such as page up, page down, selection. etc 235 | func parsingMenuOption(labels []string, menu *widgets.List, input *widgets.Paragraph, logBox *widgets.List, warning *widgets.Paragraph, uiEvents <-chan ui.Event, customWarning ...string) (int, error) { 236 | 237 | if len(labels) == 0 { 238 | return 0, fmt.Errorf("No Entry in the menu") 239 | } 240 | 241 | menuTitle := menu.Title + "---%v/%v" 242 | 243 | // first, last always point to the first and last entry in current menu page 244 | first := 0 245 | last := min(10, len(labels)) 246 | listData := labels[first:last] 247 | menu.Rows = listData 248 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 249 | ui.Render(menu) 250 | 251 | // keep tracking all input from user 252 | for { 253 | k := readKey(uiEvents) 254 | switch k { 255 | case "": 256 | return -1, ExitRequest 257 | case "": 258 | return -1, BackRequest 259 | case "": 260 | choose := input.Text 261 | input.Text = "" 262 | ui.Render(input) 263 | c, err := strconv.Atoi(choose) 264 | // Input is valid if the selected index 265 | // is between 0 <= input < len(labels) 266 | if err == nil && c >= 0 && c < len(labels) { 267 | // if there is not specific warning for this entry, return it 268 | // elsewise show the warning and continue 269 | if len(customWarning) > c && customWarning[c] != "" { 270 | warning.Text = customWarning[c] 271 | ui.Render(warning) 272 | continue 273 | } 274 | return c, nil 275 | } 276 | warning.Text = "Please enter a valid entry number." 277 | ui.Render(warning) 278 | case "": 279 | if len(input.Text) > 0 { 280 | input.Text = input.Text[:len(input.Text)-1] 281 | ui.Render(input) 282 | } 283 | case "": 284 | first = max(0, first-10) 285 | last = min(first+10, len(labels)) 286 | listData := labels[first:last] 287 | menu.Rows = listData 288 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 289 | ui.Render(menu) 290 | case "": 291 | if first+10 >= len(labels) { 292 | continue 293 | } 294 | first = first + 10 295 | last = min(first+10, len(labels)) 296 | listData := labels[first:last] 297 | menu.Rows = listData 298 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 299 | ui.Render(menu) 300 | case "": 301 | // scroll up in the log box 302 | logBox.ScrollHalfPageUp() 303 | ui.Render(logBox) 304 | case "": 305 | // scroll down in the log box 306 | logBox.ScrollHalfPageDown() 307 | ui.Render(logBox) 308 | case "", "": 309 | // move one line up 310 | first = max(0, first-1) 311 | last = min(first+10, len(labels)) 312 | listData := labels[first:last] 313 | menu.Rows = listData 314 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 315 | ui.Render(menu) 316 | case "", "": 317 | // move one line down 318 | last = min(last+1, len(labels)) 319 | first = max(0, last-10) 320 | listData := labels[first:last] 321 | menu.Rows = listData 322 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 323 | ui.Render(menu) 324 | case "": 325 | // first page 326 | first = 0 327 | last = min(first+10, len(labels)) 328 | listData := labels[first:last] 329 | menu.Rows = listData 330 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 331 | ui.Render(menu) 332 | case "": 333 | // last page 334 | last = len(labels) 335 | first = max(0, last-10) 336 | listData := labels[first:last] 337 | menu.Rows = listData 338 | menu.Title = fmt.Sprintf(menuTitle, first, len(labels)) 339 | ui.Render(menu) 340 | case "": 341 | input.Text += " " 342 | ui.Render(input) 343 | default: 344 | // the termui use a string begin at '<' to represent some special keys 345 | // for example the 'F1' key will be parsed to "" string . 346 | // we should do nothing when meet these special keys, we only care about alphabets and digits. 347 | if k[0:1] != "<" { 348 | input.Text += k 349 | ui.Render(input) 350 | } 351 | } 352 | } 353 | } 354 | 355 | // PromptMenuEntry presents all entries into a menu with numbers. 356 | // user inputs a number to choose from them. 357 | // customWarning allow self-defined warnings in the menu 358 | // for example the wifi menu want to show specific warning when user hit a specific entry, 359 | // because some wifi's type may not be supported. 360 | func PromptMenuEntry(menuTitle string, introwords string, entries []Entry, uiEvents <-chan ui.Event, menus chan<- string, customWarning ...string) (Entry, error) { 361 | menus <- menuTitle 362 | 363 | defer ui.Clear() 364 | 365 | // listData contains all choice's labels 366 | listData := []string{} 367 | for i, e := range entries { 368 | listData = append(listData, fmt.Sprintf("[%d] %s", i, e.Label())) 369 | } 370 | windowWidth, windowHeight := termbox.Size() 371 | 372 | // location will serve as the y1 coordinate in this function. 373 | location := 0 374 | menu := widgets.NewList() 375 | menu.Title = menuTitle 376 | // windowHeight is divided by 5 to make room for the five boxes that will be on the screen. 377 | height := windowHeight / 5 378 | // menu is the box with the options. It will be at the top of the screen. 379 | menu.SetRect(0, location, windowWidth, height) 380 | menu.TextStyle.Fg = ui.ColorWhite 381 | 382 | location += height 383 | // A variable to help get rid of the gap between "Choose an option:" and its 384 | // corresponding box. 385 | alternateHeight := 2 386 | 387 | intro := newParagraph(introwords, false, location, windowWidth, height) 388 | 389 | location += alternateHeight 390 | input := newParagraph("", true, location, windowWidth, height) 391 | 392 | location += height 393 | logBox := widgets.NewList() 394 | logBox.Title = "Logs:" 395 | logBox.WrapText = false 396 | logBox.SetRect(0, location, windowWidth, location+height) 397 | 398 | location += height 399 | warning := newParagraph(" to go back, to exit", false, location, windowWidth, height) 400 | 401 | // Write the contents of the log output text file to the log box. 402 | var file, err = os.OpenFile("logOutput.txt", os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644) 403 | if err != nil { 404 | log.Fatal(err) 405 | } 406 | scanner := bufio.NewScanner(file) 407 | scanner.Split(bufio.ScanLines) 408 | for scanner.Scan() { 409 | logBox.Rows = append(logBox.Rows, scanner.Text()) 410 | } 411 | if err := scanner.Err(); err != nil { 412 | log.Fatal(err) 413 | } 414 | defer file.Close() 415 | 416 | ui.Render(intro) 417 | ui.Render(input) 418 | ui.Render(warning) 419 | ui.Render(logBox) 420 | 421 | chooseIndex, err := parsingMenuOption(listData, menu, input, logBox, warning, uiEvents, customWarning...) 422 | if err != nil { 423 | return nil, err 424 | } 425 | 426 | return entries[chooseIndex], nil 427 | } 428 | 429 | func PromptConfirmation(message string, uiEvents <-chan ui.Event, menus chan<- string) (bool, error) { 430 | defer ui.Clear() 431 | menus <- message 432 | 433 | wid := resultWidth 434 | text := "" 435 | position := 1 436 | 437 | for { 438 | // Split message if longer than msg box 439 | end := min(len(message), wid) 440 | text += message[:end] + "\n" 441 | if len(message) > wid { 442 | message = message[wid:] 443 | } else { 444 | break 445 | } 446 | } 447 | 448 | text += "\n[0] Yes\n[1] No\n" 449 | position += countNewlines(text) + 2 450 | wid += 2 // 2 borders 451 | 452 | msg := newParagraph(text, true, 0, wid, position) 453 | ui.Render(msg) 454 | 455 | selectHint := newParagraph("Choose an option:", false, position+1, wid, 1) 456 | ui.Render(selectHint) 457 | 458 | entry := newParagraph("", true, position+2, wid, 3) 459 | ui.Render(entry) 460 | 461 | backHint := newParagraph(" to go back, to exit", false, position+6, wid, 1) 462 | ui.Render(backHint) 463 | 464 | for { 465 | key := readKey(uiEvents) 466 | switch key { 467 | case "": 468 | return false, BackRequest 469 | case "": 470 | return false, ExitRequest 471 | case "": 472 | switch entry.Text { 473 | case "0": 474 | return true, nil 475 | case "1": 476 | return false, nil 477 | } 478 | case "0", "1": 479 | entry.Text = key 480 | ui.Render(entry) 481 | case "": 482 | entry.Text = "" 483 | ui.Render(entry) 484 | } 485 | } 486 | } 487 | 488 | type Progress struct { 489 | paragraph *widgets.Paragraph 490 | animated bool 491 | sigTerm chan bool 492 | ackTerm chan bool 493 | } 494 | 495 | func NewProgress(text string, animated bool) Progress { 496 | paragraph := widgets.NewParagraph() 497 | paragraph.Border = true 498 | paragraph.SetRect(0, 0, resultWidth, 10) 499 | paragraph.TextStyle.Fg = ui.ColorWhite 500 | paragraph.Title = "Operation Running" 501 | paragraph.Text = text 502 | ui.Render(paragraph) 503 | 504 | progress := Progress{paragraph, animated, make(chan bool), make(chan bool)} 505 | if animated { 506 | go progress.animate() 507 | } 508 | return progress 509 | } 510 | 511 | func (p *Progress) Update(text string) { 512 | p.paragraph.Text = text 513 | ui.Render(p.paragraph) 514 | } 515 | 516 | func (p *Progress) animate() { 517 | counter := 0 518 | for { 519 | select { 520 | case <-p.sigTerm: 521 | p.ackTerm <- true 522 | return 523 | default: 524 | time.Sleep(time.Second) 525 | pText := p.paragraph.Text 526 | p.Update(pText + strings.Repeat(".", counter%4)) 527 | p.paragraph.Text = pText 528 | counter++ 529 | } 530 | } 531 | } 532 | 533 | func (p *Progress) Close() { 534 | if p.animated { 535 | p.sigTerm <- true 536 | <-p.ackTerm 537 | } 538 | ui.Clear() 539 | } 540 | -------------------------------------------------------------------------------- /pkg/menu/menu_test.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | 8 | ui "github.com/gizak/termui/v3" 9 | ) 10 | 11 | type testEntry struct { 12 | message string 13 | label string 14 | isDefault bool 15 | } 16 | 17 | func (u *testEntry) Label() string { 18 | return u.label 19 | } 20 | 21 | func (u *testEntry) IsDefault() bool { 22 | return u.isDefault 23 | } 24 | 25 | func TestNewParagraph(t *testing.T) { 26 | testText := "newParagraph test" 27 | p := newParagraph(testText, false, 0, 50, 3) 28 | if testText != p.Text { 29 | t.Errorf("Incorrect value for p.Text. got: %v, want: %v", p.Text, testText) 30 | } 31 | } 32 | 33 | func pressKey(ch chan ui.Event, input []string) { 34 | var key ui.Event 35 | for _, id := range input { 36 | key = ui.Event{ 37 | Type: ui.KeyboardEvent, 38 | ID: id, 39 | } 40 | ch <- key 41 | } 42 | } 43 | 44 | func nextMenuReady(menus <-chan string) string { 45 | return <-menus 46 | } 47 | 48 | func TestProcessInputSimple(t *testing.T) { 49 | testText := "test" 50 | uiEvents := make(chan ui.Event) 51 | go pressKey(uiEvents, []string{"t", "e", "s", "t", ""}) 52 | 53 | input, _, err := processInput("test processInput simple", 0, 50, 1, AlwaysValid, uiEvents) 54 | 55 | if err != nil { 56 | t.Errorf("ProcessInput failed: %v", err) 57 | } 58 | if input != testText { 59 | t.Errorf("Incorrect value for input. got: %v, want: %v", input, testText) 60 | } 61 | } 62 | 63 | func TestProcessInputComplex(t *testing.T) { 64 | testText := "100" 65 | uiEvents := make(chan ui.Event) 66 | // mock user input: 67 | // first input is bad input "bad" 68 | // second input is bad input "100a" 69 | // third input is good input "100" but contain a process of typo then backspace 70 | // now the warning text should be "" 71 | go pressKey(uiEvents, []string{"b", "a", "d", "", 72 | "1", "0", "0", "a", "", 73 | "1", "0", "a", "", "0", ""}) 74 | 75 | isValid := func(input string) (string, string, bool) { 76 | if _, err := strconv.ParseUint(input, 10, 32); err != nil { 77 | return "", "Input is not a valid entry number.", false 78 | } 79 | return input, "", true 80 | } 81 | 82 | input, _, err := processInput("test processInput complex", 0, 50, 1, isValid, uiEvents) 83 | if err != nil { 84 | t.Errorf("Error: %v", err) 85 | } 86 | if input != testText { 87 | t.Errorf("Incorrect value for input. got: %v, want: %v", input, testText) 88 | } 89 | } 90 | 91 | func TestProcessInputLong(t *testing.T) { 92 | uiEvents := make(chan ui.Event) 93 | testText := "splash=silent quiet root=live:CDLABEL=openSUSE_Leap_15.2_KDE_Live " + 94 | "rd.live.image rd.live.overlay.persistent rd.live.overlay.cowfs=ext4" + 95 | "iso-scan/filename=openSUSE-Leap-15.2-KDE-Live-x86_64-Build31.135-Media.iso" 96 | 97 | var keyPresses []string 98 | for i := 1; i <= len(testText); i++ { 99 | keyPresses = append(keyPresses, testText[i-1:i]) 100 | } 101 | keyPresses = append(keyPresses, "") 102 | go pressKey(uiEvents, keyPresses) 103 | 104 | input, _, err := processInput("test processInput long", 0, 50, 1, AlwaysValid, uiEvents) 105 | if err != nil { 106 | t.Errorf("Error: %v", err) 107 | } else if input != testText { 108 | t.Errorf("Incorrect value for input. got: %v, want: %v", input, testText) 109 | } 110 | } 111 | 112 | func TestDisplayResult(t *testing.T) { 113 | var longMsg []string 114 | for i := 0; i < 50; i++ { 115 | newLine := "Line " + strconv.Itoa(i) 116 | longMsg = append(longMsg, newLine) 117 | } 118 | 119 | for _, tt := range []struct { 120 | name string 121 | msg []string 122 | want string 123 | human func(chan ui.Event, <-chan string) 124 | }{ 125 | { 126 | name: "short_message", 127 | msg: []string{"short message"}, 128 | want: "short message", 129 | human: func(uiEvents chan ui.Event, menus <-chan string) { 130 | nextMenuReady(menus) 131 | pressKey(uiEvents, []string{"q"}) 132 | }, 133 | }, 134 | { 135 | // Display the long message and immediately exit 136 | name: "long_message_press_esc", 137 | msg: longMsg, 138 | want: strings.Join(longMsg[:resultHeight], "\n") + "\n\n(More)", 139 | human: func(uiEvents chan ui.Event, menus <-chan string) { 140 | nextMenuReady(menus) 141 | pressKey(uiEvents, []string{""}) 142 | }, 143 | }, 144 | { 145 | // Display the long message, scroll to the bottom, then exit 146 | name: "long message_scroll_to_end", 147 | msg: longMsg, 148 | want: strings.Join(longMsg[len(longMsg)-resultHeight:], "\n") + "\n\n(End of message)", 149 | human: func(uiEvents chan ui.Event, menus <-chan string) { 150 | nextMenuReady(menus) 151 | pressKey(uiEvents, []string{"", "", "", ""}) 152 | }, 153 | }, 154 | } { 155 | t.Run(tt.name, func(t *testing.T) { 156 | uiEvents := make(chan ui.Event) 157 | menus := make(chan string) 158 | 159 | go tt.human(uiEvents, menus) 160 | msg, err := DisplayResult(tt.msg, uiEvents, menus) 161 | 162 | if err != nil && err != BackRequest { 163 | t.Errorf("Error: %v", err) 164 | } 165 | if tt.want != msg { 166 | t.Errorf("Incorrect value for msg. got: %v, want: %v", msg, tt.want) 167 | } 168 | 169 | }) 170 | } 171 | } 172 | 173 | func TestCountNewlines(t *testing.T) { 174 | for _, tt := range []struct { 175 | name string 176 | str string 177 | want int 178 | }{ 179 | { 180 | name: "empty_string", 181 | str: "", 182 | want: 0, 183 | }, 184 | { 185 | name: "no_newline", 186 | str: "test string", 187 | want: 0, 188 | }, 189 | { 190 | name: "single_newline_end", 191 | str: "test line\n", 192 | want: 1, 193 | }, 194 | { 195 | name: "double_newline_end", 196 | str: "test line\n\n", 197 | want: 2, 198 | }, 199 | { 200 | name: "two_lines", 201 | str: "test line 1\n test line 2\n", 202 | want: 2, 203 | }, 204 | { 205 | name: "prefix_double_newline", 206 | str: "\n\n test line 2", 207 | want: 2, 208 | }, 209 | } { 210 | t.Run(tt.name, func(t *testing.T) { 211 | lines := countNewlines(tt.str) 212 | if lines != tt.want { 213 | t.Errorf("Expected %d counted lines, but got %d\n", tt.want, lines) 214 | } 215 | }) 216 | } 217 | } 218 | 219 | func TestPromptConfirmation(t *testing.T) { 220 | for _, tt := range []struct { 221 | name string 222 | wantBool bool 223 | wantErr error 224 | human func(chan ui.Event, <-chan string) 225 | }{ 226 | { 227 | name: "select_yes", 228 | wantBool: true, 229 | wantErr: nil, 230 | human: func(uiEvents chan ui.Event, menus <-chan string) { 231 | nextMenuReady(menus) 232 | pressKey(uiEvents, []string{"0", ""}) 233 | }, 234 | }, 235 | { 236 | name: "select_no", 237 | wantBool: true, 238 | wantErr: nil, 239 | human: func(uiEvents chan ui.Event, menus <-chan string) { 240 | nextMenuReady(menus) 241 | pressKey(uiEvents, []string{"0", ""}) 242 | }, 243 | }, 244 | { 245 | name: "go_back", 246 | wantBool: false, 247 | wantErr: BackRequest, 248 | human: func(uiEvents chan ui.Event, menus <-chan string) { 249 | nextMenuReady(menus) 250 | pressKey(uiEvents, []string{"", ""}) 251 | }, 252 | }, 253 | { 254 | name: "exit", 255 | wantBool: false, 256 | wantErr: ExitRequest, 257 | human: func(uiEvents chan ui.Event, menus <-chan string) { 258 | nextMenuReady(menus) 259 | pressKey(uiEvents, []string{"", ""}) 260 | }, 261 | }, 262 | { 263 | name: "change_response", 264 | wantBool: true, 265 | wantErr: nil, 266 | human: func(uiEvents chan ui.Event, menus <-chan string) { 267 | nextMenuReady(menus) 268 | pressKey(uiEvents, []string{"1", "", "0", ""}) 269 | }, 270 | }, 271 | { 272 | name: "submit_without_value", 273 | wantBool: true, 274 | wantErr: nil, 275 | human: func(uiEvents chan ui.Event, menus <-chan string) { 276 | nextMenuReady(menus) 277 | pressKey(uiEvents, []string{"1", "", "0", ""}) 278 | }, 279 | }, 280 | } { 281 | t.Run(tt.name, func(t *testing.T) { 282 | uiEvents := make(chan ui.Event) 283 | menus := make(chan string) 284 | 285 | go tt.human(uiEvents, menus) 286 | accept, err := PromptConfirmation("Continue?", uiEvents, menus) 287 | if accept != tt.wantBool { 288 | t.Errorf("Expected %t, but received %t.\n", tt.wantBool, accept) 289 | } else if err != nil && err != tt.wantErr { 290 | t.Errorf("Expected error %v, but got %v", tt.wantErr, err) 291 | } 292 | }) 293 | } 294 | } 295 | 296 | func TestDisplayMenu(t *testing.T) { 297 | entry1 := &testEntry{label: "entry 1"} 298 | entry2 := &testEntry{label: "entry 2"} 299 | entry3 := &testEntry{label: "entry 3"} 300 | entry4 := &testEntry{label: "entry 4"} 301 | entry5 := &testEntry{label: "entry 5"} 302 | entry6 := &testEntry{label: "entry 6"} 303 | entry7 := &testEntry{label: "entry 7"} 304 | entry8 := &testEntry{label: "entry 8"} 305 | entry9 := &testEntry{label: "entry 9"} 306 | entry10 := &testEntry{label: "entry 10"} 307 | entry11 := &testEntry{label: "entry 11"} 308 | entry12 := &testEntry{label: "entry 12"} 309 | 310 | for _, tt := range []struct { 311 | name string 312 | entries []Entry 313 | want Entry 314 | human func(chan ui.Event, <-chan string) 315 | }{ 316 | { 317 | name: "hit_0", 318 | entries: []Entry{entry1, entry2, entry3}, 319 | want: entry1, 320 | human: func(uiEvents chan ui.Event, menus <-chan string) { 321 | nextMenuReady(menus) 322 | pressKey(uiEvents, []string{"0", ""}) 323 | }, 324 | }, 325 | { 326 | name: "hit_1", 327 | entries: []Entry{entry1, entry2, entry3}, 328 | want: entry2, 329 | human: func(uiEvents chan ui.Event, menus <-chan string) { 330 | nextMenuReady(menus) 331 | pressKey(uiEvents, []string{"1", ""}) 332 | }, 333 | }, 334 | { 335 | name: "hit_2", 336 | entries: []Entry{entry1, entry2, entry3}, 337 | want: entry3, 338 | human: func(uiEvents chan ui.Event, menus <-chan string) { 339 | nextMenuReady(menus) 340 | pressKey(uiEvents, []string{"2", ""}) 341 | }, 342 | }, 343 | { 344 | name: "error_input_then_right_input", 345 | entries: []Entry{entry1, entry2, entry3}, 346 | want: entry2, 347 | human: func(uiEvents chan ui.Event, menus <-chan string) { 348 | nextMenuReady(menus) 349 | pressKey(uiEvents, []string{"0", "a", "", "1", ""}) 350 | }, 351 | }, 352 | { 353 | name: "exceed_the_bound_then_right_input", 354 | entries: []Entry{entry1, entry2, entry3}, 355 | want: entry1, 356 | human: func(uiEvents chan ui.Event, menus <-chan string) { 357 | nextMenuReady(menus) 358 | pressKey(uiEvents, []string{"4", "", "0", ""}) 359 | }, 360 | }, 361 | { 362 | name: "right_input_with_backspace", 363 | entries: []Entry{entry1, entry2, entry3}, 364 | want: entry3, 365 | human: func(uiEvents chan ui.Event, menus <-chan string) { 366 | nextMenuReady(menus) 367 | pressKey(uiEvents, []string{"2", "a", "", ""}) 368 | }, 369 | }, 370 | // Skipping this test due to upstream bug in termui 371 | // 372 | // See termui issue 228: "If Plot data is empty, Index out of range is thrown" 373 | // (https://github.com/gizak/termui/issues/282). 374 | // 375 | // { 376 | // name: "___hit_11", 377 | // entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 378 | // // hit -> -> current page is : 0~9 379 | // want: entry12, 380 | // human: func(uiEvents chan ui.Event, menus <-chan string) { 381 | // nextMenuReady(menus) 382 | // pressKey(uiEvents, []string{"", "", "", "1", "1", ""}) 383 | // }, 384 | // }, 385 | { 386 | name: "__exceed_the_bound_then_right_input", 387 | entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 388 | // hit -> current page is : 10~11 because the first should do nothing 389 | want: entry11, 390 | human: func(uiEvents chan ui.Event, menus <-chan string) { 391 | nextMenuReady(menus) 392 | pressKey(uiEvents, []string{"", "", "-", "1", "", "1", "0", ""}) 393 | }, 394 | }, 395 | { 396 | name: "___exceed_the_bound_then_right_input", 397 | entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 398 | // hit -> -> current page is : 1~10 399 | want: entry2, 400 | human: func(uiEvents chan ui.Event, menus <-chan string) { 401 | nextMenuReady(menus) 402 | pressKey(uiEvents, []string{"", "", "", "2", "1", "", "1", ""}) 403 | }, 404 | }, 405 | { 406 | name: "__then_right_input", 407 | entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 408 | // hit -> current page is : 2~11 because the will move to the last page 409 | want: entry5, 410 | human: func(uiEvents chan ui.Event, menus <-chan string) { 411 | nextMenuReady(menus) 412 | pressKey(uiEvents, []string{"", "4", ""}) 413 | }, 414 | }, 415 | { 416 | name: "__then_right_input", 417 | entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 418 | // hit -> current page is : 0~9 because the will move to the first page 419 | want: entry1, 420 | human: func(uiEvents chan ui.Event, menus <-chan string) { 421 | nextMenuReady(menus) 422 | pressKey(uiEvents, []string{"", "", "0", ""}) 423 | }, 424 | }, 425 | { 426 | name: "___then_right_input", 427 | entries: []Entry{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8, entry9, entry10, entry11, entry12}, 428 | // scroll mouse wheel -> -> current page is : 1~10 429 | want: entry11, 430 | human: func(uiEvents chan ui.Event, menus <-chan string) { 431 | nextMenuReady(menus) 432 | pressKey(uiEvents, []string{"", "", "", "10", ""}) 433 | }, 434 | }, 435 | } { 436 | t.Run(tt.name, func(t *testing.T) { 437 | 438 | uiEvents := make(chan ui.Event) 439 | menus := make(chan string) 440 | 441 | //1go pressKey(uiEvents, tt.userInput) 442 | go tt.human(uiEvents, menus) 443 | 444 | chosen, err := PromptMenuEntry("test menu title", tt.name, tt.entries, uiEvents, menus) 445 | 446 | if err != nil { 447 | t.Errorf("Error: %v", err) 448 | } 449 | if tt.want != chosen { 450 | t.Errorf("Incorrect choice. Choose %+v, want %+v", chosen, tt.want) 451 | } 452 | 453 | }) 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /pkg/wifi/iwl.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "regexp" 17 | "strings" 18 | "time" 19 | 20 | "github.com/u-root/webboot/pkg/wpa/passphrase" 21 | ) 22 | 23 | const ( 24 | nopassphrase = `network={ 25 | ssid="%s" 26 | proto=RSN 27 | key_mgmt=NONE 28 | }` 29 | eap = `network={ 30 | ssid="%s" 31 | key_mgmt=WPA-EAP 32 | identity="%s" 33 | password="%s" 34 | }` 35 | ) 36 | 37 | var ( 38 | // RegEx for parsing iwlist output 39 | cellRE = regexp.MustCompile("(?m)^\\s*Cell") 40 | essidRE = regexp.MustCompile("(?m)^\\s*ESSID.*") 41 | encKeyOptRE = regexp.MustCompile("(?m)^\\s*Encryption key:(on|off)$") 42 | wpa2RE = regexp.MustCompile("(?m)^\\s*IE: IEEE 802.11i/WPA2 Version 1$") 43 | authSuitesRE = regexp.MustCompile("(?m)^\\s*Authentication Suites .*$") 44 | ) 45 | 46 | type SecProto int 47 | 48 | const ( 49 | NoEnc SecProto = iota 50 | WpaPsk 51 | WpaEap 52 | NotSupportedProto 53 | ) 54 | 55 | // IWLWorker implements the WiFi interface using the Intel Wireless LAN commands 56 | type IWLWorker struct { 57 | Interface string 58 | } 59 | 60 | func NewIWLWorker(stdout, stderr io.Writer, i string) (WiFi, error) { 61 | cmd := exec.Command("ip", "link", "set", "dev", i, "up") 62 | cmd.Stdout, cmd.Stderr = stdout, stderr 63 | if err := cmd.Run(); err != nil { 64 | return &IWLWorker{""}, err 65 | } 66 | return &IWLWorker{i}, nil 67 | } 68 | 69 | func (w *IWLWorker) Scan(stdout, stderr io.Writer) ([]Option, error) { 70 | // Need a local copy of exec's output to parse out the Iwlist 71 | var execOutput bytes.Buffer 72 | stdoutTee := io.MultiWriter(&execOutput, stdout) 73 | 74 | cmd := exec.Command("iwlist", w.Interface, "scanning") 75 | cmd.Stdout, cmd.Stderr = stdoutTee, stderr 76 | if err := cmd.Run(); err != nil { 77 | return nil, err 78 | } 79 | return parseIwlistOut(execOutput.Bytes()), nil 80 | } 81 | 82 | /* 83 | * Assumptions: 84 | * 1) Cell, essid, and encryption key option are 1:1 match 85 | * 2) We only support IEEE 802.11i/WPA2 Version 1 86 | * 3) Each Wifi only support (1) authentication suites (based on observations) 87 | */ 88 | 89 | func parseIwlistOut(o []byte) []Option { 90 | cells := cellRE.FindAllIndex(o, -1) 91 | essids := essidRE.FindAll(o, -1) 92 | encKeyOpts := encKeyOptRE.FindAll(o, -1) 93 | 94 | if cells == nil { 95 | return nil 96 | } 97 | 98 | var res []Option 99 | knownEssids := make(map[string]bool) 100 | 101 | // Assemble all the Wifi options 102 | for i := 0; i < len(cells); i++ { 103 | essid := strings.Trim(strings.Split(string(essids[i]), ":")[1], "\"\n") 104 | if knownEssids[essid] { 105 | continue 106 | } 107 | knownEssids[essid] = true 108 | encKeyOpt := strings.Trim(strings.Split(string(encKeyOpts[i]), ":")[1], "\n") 109 | if encKeyOpt == "off" { 110 | res = append(res, Option{essid, NoEnc}) 111 | continue 112 | } 113 | // Find the proper Authentication Suites 114 | start, end := cells[i][0], len(o) 115 | if i != len(cells)-1 { 116 | end = cells[i+1][0] 117 | } 118 | // Narrow down the scope when looking for WPA Tag 119 | wpa2SearchArea := o[start:end] 120 | l := wpa2RE.FindIndex(wpa2SearchArea) 121 | if l == nil { 122 | res = append(res, Option{essid, NotSupportedProto}) 123 | continue 124 | } 125 | // Narrow down the scope when looking for Authorization Suites 126 | authSearchArea := wpa2SearchArea[l[0]:] 127 | authSuites := strings.Trim(strings.Split(string(authSuitesRE.Find(authSearchArea)), ":")[1], "\n ") 128 | switch authSuites { 129 | case "PSK": 130 | res = append(res, Option{essid, WpaPsk}) 131 | case "802.1x": 132 | res = append(res, Option{essid, WpaEap}) 133 | default: 134 | res = append(res, Option{essid, NotSupportedProto}) 135 | } 136 | } 137 | return res 138 | } 139 | 140 | func (w *IWLWorker) GetID(stdout, stderr io.Writer) (string, error) { 141 | var execOutput bytes.Buffer 142 | stdoutTee := io.MultiWriter(&execOutput, stdout) 143 | 144 | cmd := exec.Command("iwgetid", "-r") 145 | cmd.Stdout, cmd.Stderr = stdoutTee, stderr 146 | if err := cmd.Run(); err != nil { 147 | return "", err 148 | } 149 | return strings.Trim(execOutput.String(), " \n"), nil 150 | } 151 | 152 | func (w *IWLWorker) Connect(stdout, stderr io.Writer, a ...string) error { 153 | // format of a: [essid, pass, id] 154 | conf, err := generateConfig(a...) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | if err := ioutil.WriteFile("/tmp/wifi.conf", conf, 0444); err != nil { 160 | var file, err = os.OpenFile("logOutput.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 161 | if err != nil { 162 | return err 163 | } 164 | file.WriteString(time.Now().String() + " ") 165 | file.WriteString("/temp/wifi.conf: " + err.Error()) 166 | file.WriteString("\n") 167 | defer file.Close() 168 | return fmt.Errorf("/tmp/wifi.conf: %v", err) 169 | } 170 | 171 | // Each request has a 30 second window to make a connection 172 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 173 | defer cancel() 174 | c := make(chan error, 1) 175 | 176 | // There's no telling how long the supplicant will take, but on the other hand, 177 | // it's been almost instantaneous. But, further, it needs to keep running. 178 | go func() { 179 | cmd := exec.Command("wpa_supplicant", "-i"+w.Interface, "-c/tmp/wifi.conf") 180 | outfile, err := os.OpenFile("logOutput.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 181 | cmd.Stdout, cmd.Stderr = outfile, outfile 182 | if err != nil { 183 | log.Print(err) 184 | } 185 | defer outfile.Close() 186 | if err = cmd.Run(); err != nil { 187 | log.Print(err) 188 | fmt.Sprintf("%s %s\n", time.Now().String(), err.Error()) 189 | } 190 | c <- fmt.Errorf("wpa supplicant exited unexpectedly") 191 | 192 | }() 193 | 194 | // dhclient might never return on incorrect passwords or identity 195 | go func() { 196 | cmd := exec.CommandContext(ctx, "dhclient", "-ipv4=true", "-ipv6=false", "-v", w.Interface) 197 | 198 | outfile, err := os.OpenFile("logOutput.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 199 | cmd.Stdout, cmd.Stderr = outfile, outfile 200 | if err != nil { 201 | log.Print(err) 202 | fmt.Sprintf("%s %s\n", time.Now().String(), err.Error()) 203 | } 204 | defer outfile.Close() 205 | c <- cmd.Run() 206 | }() 207 | 208 | select { 209 | case err := <-c: 210 | return err 211 | case <-ctx.Done(): 212 | return fmt.Errorf("dhcp timeout") 213 | } 214 | } 215 | 216 | func generateConfig(a ...string) (conf []byte, err error) { 217 | // format of a: [essid, pass, id] 218 | switch { 219 | case len(a) == 3: 220 | conf = []byte(fmt.Sprintf(eap, a[0], a[2], a[1])) 221 | case len(a) == 2: 222 | conf, err = passphrase.Run(a[0], a[1]) 223 | if err != nil { 224 | return nil, fmt.Errorf("essid: %v, pass: %v : %v", a[0], a[1], err) 225 | } 226 | case len(a) == 1: 227 | conf = []byte(fmt.Sprintf(nopassphrase, a[0])) 228 | default: 229 | return nil, fmt.Errorf("generateConfig needs 1, 2, or 3 args") 230 | } 231 | return 232 | } 233 | -------------------------------------------------------------------------------- /pkg/wifi/iwl_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io/ioutil" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/u-root/webboot/pkg/wpa/passphrase" 15 | ) 16 | 17 | type GenerateConfigTestCase struct { 18 | name string 19 | args []string 20 | exp []byte 21 | err error 22 | } 23 | 24 | var ( 25 | EssidStub = "stub" 26 | IdStub = "stub" 27 | PassStub = "123456789" 28 | BadWpaPskPass = "123" 29 | expWpaPsk, _ = passphrase.Run(EssidStub, PassStub) 30 | _, expWpaPskErr = passphrase.Run(EssidStub, BadWpaPskPass) 31 | 32 | generateConfigTestcases = []GenerateConfigTestCase{ 33 | { 34 | name: "No Pass Phrase", 35 | args: []string{EssidStub}, 36 | exp: []byte(fmt.Sprintf(nopassphrase, EssidStub)), 37 | err: nil, 38 | }, 39 | { 40 | name: "WPA-PSK", 41 | args: []string{EssidStub, PassStub}, 42 | exp: expWpaPsk, 43 | err: nil, 44 | }, 45 | { 46 | name: "WPA-EAP", 47 | args: []string{EssidStub, PassStub, IdStub}, 48 | exp: []byte(fmt.Sprintf(eap, EssidStub, IdStub, PassStub)), 49 | err: nil, 50 | }, 51 | { 52 | name: "WPA-PSK Error", 53 | args: []string{EssidStub, BadWpaPskPass}, 54 | exp: nil, 55 | err: fmt.Errorf("essid: %v, pass: %v : %v", EssidStub, BadWpaPskPass, expWpaPskErr), 56 | }, 57 | { 58 | name: "Invalid Args Length Error", 59 | args: nil, 60 | exp: nil, 61 | err: fmt.Errorf("generateConfig needs 1, 2, or 3 args"), 62 | }, 63 | } 64 | ) 65 | 66 | func TestGenerateConfig(t *testing.T) { 67 | for _, test := range generateConfigTestcases { 68 | out, err := generateConfig(test.args...) 69 | if !reflect.DeepEqual(err, test.err) || !bytes.Equal(out, test.exp) { 70 | t.Logf("TEST %v", test.name) 71 | fncCall := fmt.Sprintf("genrateConfig(%v)", test.args) 72 | t.Errorf("%s\ngot:[%v, %v]\nwant:[%v, %v]", fncCall, string(out), err, string(test.exp), test.err) 73 | } 74 | } 75 | } 76 | 77 | func TestCellRE(t *testing.T) { 78 | testcases := []struct { 79 | s string 80 | exp bool 81 | }{ 82 | {"blahblahblah\n Cell 01:", true}, 83 | {"blahblahblah\n Cell 01: blah blah", true}, 84 | {"\"Cell\"", false}, 85 | {"\"blah blah Cell blah blah\"", false}, 86 | } 87 | for _, test := range testcases { 88 | if out := cellRE.MatchString(test.s); out != test.exp { 89 | t.Errorf("%s\ngot:%v\nwant:%v", test.s, out, test.exp) 90 | } 91 | } 92 | } 93 | 94 | func TestEssidRE(t *testing.T) { 95 | testcases := []struct { 96 | s string 97 | exp bool 98 | }{ 99 | {"blahblahblah\n ESSID:\"stub\"", true}, 100 | {"blahblahblah\n ESSID:\"stub\"\n", true}, 101 | {"blahblahblah\n ESSID:\"stub-stub\"", true}, 102 | {"blahblahblah\n ESSID:\"stub-stub\"\n", true}, 103 | {"blah blah ESSID blah", false}, 104 | } 105 | for _, test := range testcases { 106 | if out := essidRE.MatchString(test.s); out != test.exp { 107 | t.Errorf("%s\ngot:%v\nwant:%v", test.s, out, test.exp) 108 | } 109 | } 110 | } 111 | 112 | func TestEncKeyOptRE(t *testing.T) { 113 | testcases := []struct { 114 | s string 115 | exp bool 116 | }{ 117 | {"blahblahblah\n Encryption key:on\n", true}, 118 | {"blahblahblah\n Encryption key:on", true}, 119 | {"blahblahblah\n Encryption key:off\n", true}, 120 | {"blahblahblah\n Encryption key:off", true}, 121 | {"blah blah Encryption key blah blah", false}, 122 | {"blah blah Encryption key:on blah blah", false}, 123 | {"blah blah Encryption key:off blah blah", false}, 124 | } 125 | for _, test := range testcases { 126 | if out := encKeyOptRE.MatchString(test.s); out != test.exp { 127 | t.Errorf("%s\ngot:%v\nwant:%v", test.s, out, test.exp) 128 | } 129 | } 130 | } 131 | 132 | func TestWpa2RE(t *testing.T) { 133 | testcases := []struct { 134 | s string 135 | exp bool 136 | }{ 137 | {"blahblahblah\n IE: IEEE 802.11i/WPA2 Version 1\n", true}, 138 | {"blahblahblah\n IE: IEEE 802.11i/WPA2 Version 1", true}, 139 | {"blah blah IE: IEEE 802.11i/WPA2 Version 1", false}, 140 | } 141 | for _, test := range testcases { 142 | if out := wpa2RE.MatchString(test.s); out != test.exp { 143 | t.Errorf("%s\ngot:%v\nwant:%v", test.s, out, test.exp) 144 | } 145 | } 146 | } 147 | 148 | func TestAuthSuitesRE(t *testing.T) { 149 | testcases := []struct { 150 | s string 151 | exp bool 152 | }{ 153 | {"blahblahblah\n Authentication Suites (1) : 802.1x\n", true}, 154 | {"blahblahblah\n Authentication Suites (1) : 802.1x", true}, 155 | {"blahblahblah\n Authentication Suites (1) : PSK\n", true}, 156 | {"blahblahblah\n Authentication Suites (1) : PSK\n", true}, 157 | {"blahblahblah\n Authentication Suites (2) : blah, blah\n", true}, 158 | {"blahblahblah\n Authentication Suites (1) : other protocol\n", true}, 159 | {"blahblahblah\n Authentication Suites (1) : other protocol", true}, 160 | {"blah blah Authentication Suites : blah blah", false}, 161 | } 162 | for _, test := range testcases { 163 | if out := authSuitesRE.MatchString(test.s); out != test.exp { 164 | t.Errorf("%s\ngot:%v\nwant:%v", test.s, out, test.exp) 165 | } 166 | } 167 | } 168 | 169 | func TestParseIwlistOutput(t *testing.T) { 170 | var ( 171 | o []byte 172 | exp, out []Option 173 | err error 174 | ) 175 | 176 | // No WiFi present 177 | o = nil 178 | exp = nil 179 | out = parseIwlistOut(o) 180 | if !reflect.DeepEqual(out, exp) { 181 | t.Errorf("\ngot:[%v]\nwant:[%v]", out, exp) 182 | } 183 | 184 | // Only 1 WiFi present 185 | o = []byte(` 186 | wlan0 Scan completed : 187 | Cell 01 - Address: 00:00:00:00:00:01 188 | Channel:001 189 | Frequency:5.58 GHz (Channel 001) 190 | Quality=1/2 Signal level=-23 dBm 191 | Encryption key:on 192 | ESSID:"stub-wpa-eap-1" 193 | Bit Rates:36 Mb/s; 48 Mb/s; 54 Mb/s 194 | Mode:Master 195 | Extra:tsf=000000000000000000 196 | Extra: Last beacon: 1260ms ago 197 | IE: Unknown: 000000000000000000 198 | IE: Unknown: 000000000000000000 199 | IE: Unknown: 000000000000000000 200 | IE: IEEE 802.11i/WPA2 Version 1 201 | Group Cipher : CCMP 202 | Pairwise Ciphers (1) : CCMP 203 | Authentication Suites (1) : 802.1x 204 | IE: Unknown: 000000000000000000 205 | IE: Unknown: 000000000000000000 206 | IE: Unknown: 000000000000000000 207 | IE: Unknown: 000000000000000000 208 | IE: Unknown: 000000000000000000 209 | `) 210 | exp = []Option{ 211 | {"stub-wpa-eap-1", WpaEap}, 212 | } 213 | out = parseIwlistOut(o) 214 | if !reflect.DeepEqual(out, exp) { 215 | t.Errorf("\ngot:[%v]\nwant:[%v]", out, exp) 216 | } 217 | 218 | // Regular scenarios (many choices) 219 | exp = []Option{ 220 | {"stub-wpa-eap-1", WpaEap}, 221 | {"stub-rsa-1", NoEnc}, 222 | {"stub-wpa-psk-1", WpaPsk}, 223 | {"stub-rsa-2", NoEnc}, 224 | {"stub-wpa-psk-2", WpaPsk}, 225 | } 226 | o, err = ioutil.ReadFile("iwlistStubOutput.txt") 227 | if err != nil { 228 | t.Errorf("error reading iwlistStubOutput.txt: %v", err) 229 | } 230 | out = parseIwlistOut(o) 231 | if !reflect.DeepEqual(out, exp) { 232 | t.Errorf("\ngot:[%v]\nwant:[%v]", out, exp) 233 | } 234 | } 235 | 236 | func BenchmarkParseIwlistOutput(b *testing.B) { 237 | // Set Up 238 | o, err := ioutil.ReadFile("iwlistStubOutput.txt") 239 | if err != nil { 240 | b.Errorf("error reading iwlistStubOutput.txt: %v", err) 241 | } 242 | for i := 0; i < b.N; i++ { 243 | parseIwlistOut(o) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /pkg/wifi/iwlistStubOutput.txt: -------------------------------------------------------------------------------- 1 | wlan0 Scan completed : 2 | Cell 01 - Address: 00:00:00:00:00:01 3 | Channel:001 4 | Frequency:5.58 GHz (Channel 001) 5 | Quality=1/2 Signal level=-23 dBm 6 | Encryption key:on 7 | ESSID:"stub-wpa-eap-1" 8 | Bit Rates:36 Mb/s; 48 Mb/s; 54 Mb/s 9 | Mode:Master 10 | Extra:tsf=000000000000000000 11 | Extra: Last beacon: 1260ms ago 12 | IE: Unknown: 000000000000000000 13 | IE: Unknown: 000000000000000000 14 | IE: Unknown: 000000000000000000 15 | IE: IEEE 802.11i/WPA2 Version 1 16 | Group Cipher : CCMP 17 | Pairwise Ciphers (1) : CCMP 18 | Authentication Suites (1) : 802.1x 19 | IE: Unknown: 000000000000000000 20 | IE: Unknown: 000000000000000000 21 | IE: Unknown: 000000000000000000 22 | IE: Unknown: 000000000000000000 23 | IE: Unknown: 000000000000000000 24 | Cell 02 - Address: 00:00:00:00:00:02 25 | Channel:002 26 | Frequency:5.58 GHz (Channel 001) 27 | Quality=1/2 Signal level=-23 dBm 28 | Encryption key:on 29 | ESSID:"stub-wpa-eap-1" 30 | Bit Rates:36 Mb/s; 48 Mb/s; 54 Mb/s 31 | Mode:Master 32 | Extra:tsf=000000000000000000 33 | Extra: Last beacon: 1260ms ago 34 | IE: Unknown: 0000000000000000000000000 35 | IE: Unknown: 0000000 36 | IE: Unknown: 000000000000000000 37 | IE: IEEE 802.11i/WPA2 Version 1 38 | Group Cipher : CCMP 39 | Pairwise Ciphers (1) : CCMP 40 | Authentication Suites (1) : 802.1x 41 | IE: Unknown: 000000000000000000 42 | IE: Unknown: 000000000000000000000 43 | IE: Unknown: 00000000000000000000 44 | IE: Unknown: 000000000000000 45 | IE: Unknown: 000000000000000000 46 | IE: Unknown: 000000000000000000 47 | IE: Unknown: 0000000000000000000 48 | IE: Unknown: 00000000000000000000000 49 | IE: Unknown: 000000000000000000 50 | Cell 03 - Address: 00:00:00:00:00:03 51 | Channel:003 52 | Frequency:5.785 GHz 53 | Quality=50/70 Signal level=-60 dBm 54 | Encryption key:off 55 | ESSID:"stub-rsa-1" 56 | Bit Rates:36 Mb/s; 48 Mb/s; 54 Mb/s 57 | Mode:Master 58 | Extra:tsf=000000000000000000 59 | Extra: Last beacon: 188ms ago 60 | IE: Unknown: 0000000000000000000000000 61 | IE: Unknown: 0000000 62 | IE: Unknown: 000000000000000000 63 | IE: Unknown: 000000000000000000 64 | IE: Unknown: 000000000000000000000 65 | IE: Unknown: 00000000000000000000 66 | IE: Unknown: 000000000000000 67 | IE: Unknown: 000000000000000000 68 | IE: Unknown: 000000000000000000 69 | IE: Unknown: 0000000000000000000 70 | IE: Unknown: 00000000000000000000000 71 | IE: Unknown: 000000000000000000 72 | Cell 04 - Address: 00:00:00:00:00:04 73 | Channel:004 74 | Frequency:5.785 GHz 75 | Quality=50/70 Signal level=-60 dBm 76 | Encryption key:on 77 | ESSID:"stub-wpa-psk-1" 78 | Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s 79 | Mode:Master 80 | Extra:tsf=000000000000000000 81 | Extra: Last beacon: 188ms ago 82 | IE: Unknown: 0000000000000000000000000 83 | IE: Unknown: 0000000 84 | IE: Unknown: 000000000000000000 85 | IE: Unknown: 000000000000000000 86 | IE: Unknown: 000000000000000000000 87 | IE: Unknown: 00000000000000000000 88 | IE: Unknown: 000000000000000 89 | IE: Unknown: 000000000000000000 90 | IE: Unknown: 000000000000000000 91 | IE: Unknown: 0000000000000000000 92 | IE: Unknown: 00000000000000000000000 93 | IE: Unknown: 000000000000000000 94 | IE: IEEE 802.11i/WPA2 Version 1 95 | Group Cipher : CCMP 96 | Pairwise Ciphers (1) : CCMP 97 | Authentication Suites (1) : PSK 98 | Cell 05 - Address: 00:00:00:00:00:05 99 | Channel:005 100 | Frequency:2.412 GHz (Channel 1) 101 | Quality=48/70 Signal level=-62 dBm 102 | Encryption key:off 103 | ESSID:"stub-rsa-2" 104 | Bit Rates:36 Mb/s; 48 Mb/s; 54 Mb/s 105 | Mode:Master 106 | Extra:tsf=000000000000000000 107 | Extra: Last beacon: 3416ms ago 108 | IE: Unknown: 0000000000000000000000000 109 | IE: Unknown: 0000000 110 | IE: Unknown: 000000000000000000 111 | IE: Unknown: 000000000000000000 112 | IE: Unknown: 000000000000000000000 113 | IE: Unknown: 00000000000000000000 114 | IE: Unknown: 000000000000000 115 | IE: Unknown: 000000000000000000 116 | IE: Unknown: 000000000000000000 117 | IE: Unknown: 0000000000000000000 118 | IE: Unknown: 00000000000000000000000 119 | IE: Unknown: 000000000000000000 120 | Cell 06 - Address: 00:00:00:00:00:06 121 | Channel:006 122 | Frequency:5.785 GHz 123 | Quality=50/70 Signal level=-60 dBm 124 | Encryption key:on 125 | ESSID:"stub-wpa-psk-2" 126 | Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s 127 | Mode:Master 128 | Extra:tsf=000000000000000000 129 | Extra: Last beacon: 188ms ago 130 | IE: Unknown: 0000000000000000000000000 131 | IE: Unknown: 0000000 132 | IE: Unknown: 000000000000000000 133 | IE: Unknown: 000000000000000000 134 | IE: Unknown: 000000000000000000000 135 | IE: Unknown: 00000000000000000000 136 | IE: Unknown: 000000000000000 137 | IE: Unknown: 000000000000000000 138 | IE: Unknown: 000000000000000000 139 | IE: Unknown: 0000000000000000000 140 | IE: Unknown: 00000000000000000000000 141 | IE: Unknown: 000000000000000000 142 | IE: IEEE 802.11i/WPA2 Version 1 143 | Group Cipher : CCMP 144 | Pairwise Ciphers (1) : CCMP 145 | Authentication Suites (1) : PSK 146 | IE: Unknown: 000000000000000000 147 | IE: Unknown: 000000000000000000 148 | IE: Unknown: 0000000000000000000 149 | IE: Unknown: 00000000000000000000000 150 | IE: Unknown: 000000000000000000 151 | Cell 07 - Address: 00:00:00:00:00:07 152 | Channel:007 153 | Frequency:5.785 GHz 154 | Quality=50/70 Signal level=-60 dBm 155 | Encryption key:on 156 | ESSID:"stub-wpa-psk-2" 157 | Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s 158 | Mode:Master 159 | Extra:tsf=000000000000000000 160 | Extra: Last beacon: 188ms ago 161 | IE: Unknown: 0000000000000000000000000 162 | IE: Unknown: 0000000 163 | IE: Unknown: 000000000000000000 164 | IE: Unknown: 000000000000000000 165 | IE: Unknown: 000000000000000000000 166 | IE: Unknown: 00000000000000000000 167 | IE: Unknown: 000000000000000 168 | IE: Unknown: 000000000000000000 169 | IE: Unknown: 000000000000000000 170 | IE: Unknown: 0000000000000000000 171 | IE: Unknown: 00000000000000000000000 172 | IE: Unknown: 000000000000000000 173 | IE: IEEE 802.11i/WPA2 Version 1 174 | Group Cipher : CCMP 175 | Pairwise Ciphers (1) : CCMP 176 | Authentication Suites (1) : PSK 177 | IE: Unknown: 000000000000000000 178 | IE: Unknown: 000000000000000000 179 | IE: Unknown: 0000000000000000000 180 | IE: Unknown: 0000000000000000000000 181 | IE: Unknown: 00000000000000000000 182 | IE: Unknown: 000000000000000 183 | IE: Unknown: 000000000000000000 184 | IE: Unknown: 000000000000000000 185 | -------------------------------------------------------------------------------- /pkg/wifi/native.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | type NativeWorker struct { 15 | Interface string 16 | FD int 17 | Range IWRange 18 | } 19 | 20 | func NewNativeWorker(stdout, stderr io.Writer, i string) (WiFi, error) { 21 | s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return &NativeWorker{FD: s, Interface: i}, nil 26 | } 27 | 28 | func (w *NativeWorker) Scan(stdout, stderr io.Writer) ([]Option, error) { 29 | return nil, fmt.Errorf("Not Yet") 30 | } 31 | 32 | func (w *NativeWorker) GetID(stdout, stderr io.Writer) (string, error) { 33 | return "", fmt.Errorf("Not Yet") 34 | } 35 | 36 | func (w *NativeWorker) Connect(stdout, stderr io.Writer, a ...string) error { 37 | _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(w.FD), SIOCGIWRANGE, uintptr(unsafe.Pointer(&w.Range))) 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /pkg/wifi/native_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestNative(t *testing.T) { 13 | // Some things may fail as there may be no wlan or we might not 14 | // have the right privs. So just bail out of the test if some early 15 | // ops fail. 16 | var stdout, stderr bytes.Buffer 17 | w, err := NewNativeWorker(&stdout, &stderr, "wlan0") 18 | if err != nil { 19 | t.Log(err) 20 | return 21 | } 22 | t.Logf("Native is %v", w) 23 | err = w.Connect(&stdout, &stderr) 24 | if err != nil { 25 | t.Log(err) 26 | return 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/wifi/stub.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import "io" 8 | 9 | var _ = WiFi(&StubWorker{}) 10 | 11 | type StubWorker struct { 12 | Options []Option 13 | ID string 14 | } 15 | 16 | func NewStubWorker(stdout, stderr io.Writer, id string, options ...Option) (WiFi, error) { 17 | return &StubWorker{ID: id, Options: options}, nil 18 | } 19 | 20 | func (w *StubWorker) Scan(stdout, stderr io.Writer) ([]Option, error) { 21 | return w.Options, nil 22 | } 23 | 24 | func (w *StubWorker) GetID(stdout, stderr io.Writer) (string, error) { 25 | return w.ID, nil 26 | } 27 | 28 | func (*StubWorker) Connect(stdout, stderr io.Writer, a ...string) error { 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/wifi/types.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package wifi 6 | 7 | import "io" 8 | 9 | type Option struct { 10 | Essid string 11 | AuthSuite SecProto 12 | } 13 | 14 | type WiFi interface { 15 | Scan(stdout, stderr io.Writer) ([]Option, error) 16 | GetID(stdout, stderr io.Writer) (string, error) 17 | Connect(stdout, stderr io.Writer, a ...string) error 18 | } 19 | -------------------------------------------------------------------------------- /pkg/wpa/passphrase/passphrase.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package passphrase 6 | 7 | import ( 8 | "crypto/sha1" 9 | "encoding/hex" 10 | "fmt" 11 | 12 | "golang.org/x/crypto/pbkdf2" 13 | ) 14 | 15 | const ( 16 | MinPassLen = 8 17 | MaxPassLen = 63 18 | ResultFormat = `network={ 19 | ssid="%s" 20 | #psk="%s" 21 | psk=%s 22 | } 23 | ` 24 | ) 25 | 26 | func errorCheck(essid string, pass string) error { 27 | if len(pass) < MinPassLen || len(pass) > MaxPassLen { 28 | return fmt.Errorf("Passphrase must be 8..63 characters") 29 | } 30 | if len(essid) == 0 { 31 | return fmt.Errorf("essid cannot be empty") 32 | } 33 | return nil 34 | } 35 | 36 | func Run(essid string, pass string) ([]byte, error) { 37 | if err := errorCheck(essid, pass); err != nil { 38 | return nil, err 39 | } 40 | 41 | // There is a possible security bug here because the salt is the essid which is 42 | // static and shared across access points. Thus this salt is not sufficiently random. 43 | // This issue has been reported to the responsible parties. Since this matches the 44 | // current implementation of wpa_passphrase.c, this will maintain until further notice. 45 | pskBinary := pbkdf2.Key([]byte(pass), []byte(essid), 4096, 32, sha1.New) 46 | pskHexString := hex.EncodeToString(pskBinary) 47 | return []byte(fmt.Sprintf(ResultFormat, essid, pass, pskHexString)), nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/wpa/passphrase/passphrase_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package passphrase 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | type RunTestCase struct { 14 | name string 15 | essid string 16 | pass string 17 | out string 18 | err error 19 | } 20 | 21 | var ( 22 | essidStub = "stub" 23 | shortPass = "aaaaaaa" // 7 chars 24 | longPass = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 64 chars 25 | validPass = "aaaaaaaaaaaaaaaa" // 16 chars 26 | correctOutput = `network={ 27 | ssid="stub" 28 | #psk="aaaaaaaaaaaaaaaa" 29 | psk=e270ba95a72c6d922e902f65dfa23315f7ba43b69debc75167254acd778f2fe9 30 | } 31 | ` 32 | runTestCases = []RunTestCase{ 33 | { 34 | name: "No essid", 35 | essid: "", 36 | pass: validPass, 37 | out: "", 38 | err: fmt.Errorf("essid cannot be empty"), 39 | }, 40 | { 41 | name: "pass length is less than 8 chars", 42 | essid: essidStub, 43 | pass: shortPass, 44 | out: "", 45 | err: fmt.Errorf("Passphrase must be 8..63 characters"), 46 | }, 47 | { 48 | name: "pass length is more than 63 chars", 49 | essid: essidStub, 50 | pass: longPass, 51 | out: "", 52 | err: fmt.Errorf("Passphrase must be 8..63 characters"), 53 | }, 54 | { 55 | name: "Correct Input", 56 | essid: essidStub, 57 | pass: validPass, 58 | out: correctOutput, 59 | err: nil, 60 | }, 61 | } 62 | ) 63 | 64 | func TestRun(t *testing.T) { 65 | for _, test := range runTestCases { 66 | out, err := Run(test.essid, test.pass) 67 | if !reflect.DeepEqual(err, test.err) || string(out) != test.out { 68 | t.Errorf("TEST %s\ngot:[%v, %v]\nwant:[%v, %v]", test.name, err, string(out), test.err, string(test.out)) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /roadmap.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | Last updated June 23, 2021. 4 | 5 | * **Directory server**: Instead of storing the list of distros in 6 | cmds/webboot/types.go, download the list as a json file over HTTP (possibly 7 | from github). 8 | * **Improve UI error messaging**: Devise a better paradigm to display useful 9 | error messages to the user while still having highly technical logs 10 | available. 11 | * **ISO builder**: Add a tool to automatically build the ISO image. Right now, 12 | you have to get comfortable with fdisk and dd in order to install webboot. 13 | * **Get rid of C-based kexec** 14 | * **CentOS 8** 15 | * **ISO signing support** 16 | -------------------------------------------------------------------------------- /run-webboot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # https://docs.pmem.io/persistent-memory/getting-started-guide/creating-development-environments/virtualization/qemu 4 | # PMEM *must* be less than RAM so the system has some memory to work with 5 | export PMEM_SIZE=1G 6 | export RAM_SIZE=4G 7 | export KERNEL=$1 8 | export INITRD=$2 9 | 10 | qemu-system-x86_64 \ 11 | -machine q35 \ 12 | -m $RAM_SIZE \ 13 | -object rng-random,filename=/dev/urandom,id=rng0 \ 14 | -device virtio-rng-pci,rng=rng0 \ 15 | -netdev user,id=network0 -device rtl8139,netdev=network0 \ 16 | -kernel $KERNEL \ 17 | -initrd $INITRD \ 18 | -append "console=ttyS0 vga=786 memmap=$PMEM_SIZE!$PMEM_SIZE" \ 19 | -serial stdio 20 | -------------------------------------------------------------------------------- /syslinux.cfg.example: -------------------------------------------------------------------------------- 1 | DEFAULT webboot 2 | SAY Now booting webboot 3 | PROMPT 1 4 | TIMEOUT 1 5 | 6 | LABEL webboot 7 | KERNEL /boot/webboot 8 | INITRD /boot/webboot.cpio.gz 9 | APPEND earlyprintk=tty0 earlyprintk=ttyS0,115200,keep console=ttyS0 console=tty0 memmap=1G!512M vga=ask 10 | 11 | 12 | -------------------------------------------------------------------------------- /tips.md: -------------------------------------------------------------------------------- 1 | # Tips and Tricks 2 | 3 | ## Virtual Console 4 | 5 | * Scroll up: Shift + PgUp 6 | * Scroll down: Shift + PgDown 7 | * History: `cat /dev/vcsu` 8 | * You can get a copy of the text off the machine with: 9 | * `mkdir www` 10 | * `cd www` 11 | * `cp /dev/vcsu .` 12 | * `ip a` and take a note of the IP address. 13 | * `srvfiles -h 0.0.0.0 -p 80` 14 | * Then on the same LAN, open `http://192.168.x.y/vcsu` in a web browser. 15 | 16 | ## Setting up Vim for Go 17 | 18 | Here are some quick instructions for installing https://github.com/fatih/vim-go 19 | 20 | ``` 21 | # First install the vim plugin manager: 22 | curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim 23 | 24 | # Add the vim-go plugin to the vimrc: 25 | echo "call plug#begin('~/.vim/plugged')" >> ~/.vimrc 26 | echo "Plug 'fatih/vim-go', { 'do': ':GoUpdateBinaries' }" >> ~/.vimrc 27 | echo "call plug#end()" >> ~/.vimrc 28 | 29 | # Install the plugins: 30 | vim +PlugInstall 31 | ``` 32 | 33 | See the Run-It/Build-It/Fix-It/Test-It/... sections in 34 | https://github.com/fatih/vim-go/wiki/Tutorial 35 | 36 | You can setup shortcuts in your vimrc like this: 37 | 38 | ``` 39 | map :GoBuild 40 | map :GoTest 41 | ``` 42 | 43 | ## Running Go Debugger (Delve) 44 | 45 | This depends on the previous steps to setup Go for Vim. 46 | 47 | Adding this line to your vimrc makes the experience better: 48 | 49 | ``` 50 | echo "let g:go_debug_log_output = 0" >> ~/.vimrc 51 | ``` 52 | 53 | 1. Start the debugger with `:GoDebugStart`. 54 | 2. Set a breakpoint on the current line with `:GoDebugBreakpoint` or ``. 55 | 3. Run to the breakpoint with `:GoDebugContinue` or ``. 56 | 4. You should see the following windows: 57 | * STACKTRACE: Hit enter on any of these to jump to the code. 58 | * VARIABLES: Local variables, arguments and registers 59 | * GOROUTINES: Hit enter on any of these to jump to the code. 60 | * OUTPUT: Output from the program and dlv 61 | 5. Use these commands to navigate your code (see `:help GoDebugStart`): 62 | * `:GoDebugNext` or ``: Run to next line in the current function 63 | * `:GoDebugStep` or ``: Run to next line which may be in another 64 | function (due to a function call). 65 | * `:GoDebugStepOut`: Run until the current function returns 66 | * `:GoDebugSet {var} {value}`: Only works for ints, bool, float and pointers. 67 | * `:GoDebugPrint {expr}`: Print the result of the expression. 68 | * ``: Print the value of word under the cursor. 69 | 6. Stop the debugger with `:GoDebugStop`. 70 | 71 | ## Dumping Stack Trace of Go Program 72 | 73 | 1. Use CTRL-Z to move the process to background. 74 | 2. Send the ABRT signal `kill -ABRT %1` 75 | 3. Resume the job with `fg`. 76 | -------------------------------------------------------------------------------- /webboot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019-2021 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | package main 5 | 6 | // This program depends on the presence of the u-root project. 7 | // First time use requires that you run 8 | // go get -u github.com/u-root/u-root 9 | import ( 10 | "bufio" 11 | "flag" 12 | "fmt" 13 | "io" 14 | "log" 15 | "net/http" 16 | "os" 17 | "os/exec" 18 | "path/filepath" 19 | "regexp" 20 | "runtime" 21 | "strings" 22 | ) 23 | 24 | type cmd struct { 25 | args []string 26 | dir string 27 | } 28 | 29 | var ( 30 | debug = func(string, ...interface{}) {} 31 | 32 | verbose = flag.Bool("v", true, "verbose debugging output") 33 | uroot = flag.String("u", "", "options for u-root") 34 | cmds = flag.String("c", "", "u-root commands to build into the image") 35 | bzImage = flag.String("bzImage", "", "Optional bzImage to embed in the initramfs") 36 | iso = flag.String("iso", "", "Optional iso (e.g. tinycore.iso) to embed in the initramfs") 37 | wifi = flag.Bool("wifi", true, "include wifi tools") 38 | wpaVersion = flag.String("wpa-version", "system", "if set, download and build the wpa_supplicant (ex: 2.9)") 39 | ) 40 | 41 | func init() { 42 | flag.Parse() 43 | if *verbose { 44 | debug = log.Printf 45 | } 46 | } 47 | 48 | // This function is a bit nasty but we'll need it until we can extend 49 | // u-root a bit. Consider it a hack to get us ready for OSFC. 50 | // the Must means it has to succeed or we die. 51 | func extraBinMust(n string) string { 52 | p, err := exec.LookPath(n) 53 | if err != nil { 54 | log.Fatalf("extraMustBin(%q): %v", n, err) 55 | } 56 | debug("Using %q from %q", n, p) 57 | return p 58 | } 59 | 60 | // buildWPASupplicant downloads and builds the wpa_supplicant (and other tools) 61 | // statically. The path containing these tools is returned. 62 | func buildWPASupplicant(version string) (string, error) { 63 | // Download and extract the tar release. 64 | url := fmt.Sprintf("https://w1.fi/releases/wpa_supplicant-%s.tar.gz", version) 65 | file := fmt.Sprintf("wpa_supplicant-%s.tar.gz", version) 66 | extractDir := fmt.Sprintf("wpa_supplicant-%s", version) 67 | workDir := filepath.Join(extractDir, "wpa_supplicant") 68 | if _, err := os.Stat(extractDir); os.IsNotExist(err) { 69 | // Download file. 70 | if err := downloadFile(url, file); err != nil { 71 | return "", err 72 | } 73 | 74 | // Extract the tar file. 75 | debug("Extracting %q to %q...", file, extractDir) 76 | tarArgs := []string{"-x", "-f", file} 77 | if *verbose { 78 | tarArgs = append(tarArgs, "-v") 79 | } 80 | cmd := exec.Command("tar", tarArgs...) 81 | if *verbose { 82 | cmd.Stdout = os.Stdout 83 | } 84 | cmd.Stderr = os.Stderr 85 | if err := cmd.Run(); err != nil { 86 | return "", fmt.Errorf("Failed to extract %q: %v", file, err) 87 | } 88 | } else if err == nil { 89 | log.Printf("Directory %q already exists, skipping download", extractDir) 90 | } else { 91 | return "", fmt.Errorf("error with stat on %q: %v", extractDir, err) 92 | } 93 | 94 | wantFiles := []string{ 95 | filepath.Join(workDir, "wpa_supplicant"), 96 | filepath.Join(workDir, "wpa_cli"), 97 | filepath.Join(workDir, "wpa_passphrase"), 98 | } 99 | if err := checkFilesExist(wantFiles); err == nil { 100 | log.Printf("Files %v already exist, skipping build", wantFiles) 101 | } else { 102 | debug("Building wpa_supplicant...") 103 | // Use the defconfig. Everything related to DBUS is stripped out 104 | // because DBUS breaks the static build. 105 | origin := filepath.Join(workDir, "defconfig") 106 | destination := filepath.Join(workDir, ".config") 107 | if err := filterFile(origin, destination, regexp.MustCompile("DBUS")); err != nil { 108 | return "", fmt.Errorf("error creating .config: %v", err) 109 | } 110 | 111 | // Build with the following options: 112 | // -Os: Optimize for size 113 | // -flto: Link time optimization (reduces size) 114 | // -static: No dynamic dependencies 115 | // -pthread: Use gcc's version of pthreads to be static 116 | // -s: Strip symbols 117 | cmd := exec.Command("make", fmt.Sprintf("-j%d", runtime.NumCPU()), "EXTRA_CFLAGS=-Os -flto", "LDFLAGS=-static -pthread -Os -flto -s") 118 | cmd.Dir = workDir 119 | debug("cd %q && %s", workDir, cmd) 120 | if *verbose { 121 | cmd.Stdout = os.Stdout 122 | } 123 | cmd.Stderr = os.Stderr 124 | if err := cmd.Run(); err != nil { 125 | return "", fmt.Errorf("failed to compile wpa_supplicant: %v", err) 126 | } 127 | 128 | if err := checkFilesExist(wantFiles); err != nil { 129 | return "", fmt.Errorf("failed to build files %v, they do not exist: %v", wantFiles, err) 130 | } 131 | } 132 | 133 | return workDir, nil 134 | } 135 | 136 | // downloadFile download from the given url to the given file. 137 | func downloadFile(url, file string) error { 138 | debug("Downloading %q to %q...", url, file) 139 | 140 | resp, err := http.Get(url) 141 | if err != nil { 142 | return err 143 | } 144 | defer resp.Body.Close() 145 | if resp.StatusCode != http.StatusOK { 146 | return fmt.Errorf("error downloading %q: %s", url, resp.Status) 147 | } 148 | 149 | f, err := os.Create(file) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | if _, err = io.Copy(f, resp.Body); err != nil { 155 | f.Close() 156 | return err 157 | } 158 | return f.Close() 159 | } 160 | 161 | // checkFilesExist if each file exist. 162 | func checkFilesExist(files []string) error { 163 | for _, f := range files { 164 | if _, err := os.Stat(f); os.IsNotExist(err) { 165 | return fmt.Errorf("%q does not exist", f) 166 | } else if err != nil { 167 | return fmt.Errorf("error with stat on %q: %v", f, err) 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | // filterFile copies a file from origin to destination while deleting matching lines. 174 | func filterFile(origin, destination string, filterOut *regexp.Regexp) error { 175 | // Open the files. 176 | originF, err := os.Open(origin) 177 | if err != nil { 178 | return err 179 | } 180 | defer originF.Close() 181 | destF, err := os.Create(destination) 182 | if err != nil { 183 | return err 184 | } 185 | 186 | // Copy the lines. 187 | s := bufio.NewScanner(originF) 188 | for s.Scan() { 189 | line := s.Text() 190 | if !filterOut.MatchString(line) { 191 | if _, err := destF.WriteString(line + "\n"); err != nil { 192 | destF.Close() 193 | return err 194 | } 195 | } 196 | } 197 | if err := s.Err(); err != nil { 198 | destF.Close() 199 | return err 200 | } 201 | return destF.Close() 202 | } 203 | 204 | func main() { 205 | if _, err := os.Stat("u-root"); err != nil { 206 | c := exec.Command("git", "clone", "--single-branch", "https://github.com/u-root/u-root") 207 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 208 | if err := c.Run(); err != nil { 209 | log.Fatalf("cloning u-root: %v", err) 210 | } 211 | c = exec.Command("go", "build", ".") 212 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 213 | c.Dir = "u-root" 214 | if err := c.Run(); err != nil { 215 | log.Fatalf("building u-root/.: %v", err) 216 | } 217 | 218 | } 219 | 220 | // Use the system wpa_supplicant or download them. 221 | if *wpaVersion != "system" { 222 | wpaSupplicantPath, err := buildWPASupplicant(*wpaVersion) 223 | if err != nil { 224 | log.Fatalf("Error building wpa_supplicant: %v", err) 225 | } 226 | // Add to front of PATH to be picked up later. 227 | if err := os.Setenv("PATH", fmt.Sprintf("%s:%s", wpaSupplicantPath, os.Getenv("PATH"))); err != nil { 228 | log.Fatalf("Error setting PATH env variable: %v", err) 229 | } 230 | } 231 | 232 | var args = []string{ 233 | "./u-root/u-root", "-files", "/etc/ssl/certs", "-uroot-source=./u-root/", 234 | } 235 | 236 | // Try to find the system kexec. We can not use LookPath as people 237 | // building this might have the u-root kexec in their path. 238 | if _, err := os.Stat("/sbin/kexec"); err == nil { 239 | args = append(args, "-files=/sbin/kexec") 240 | } 241 | 242 | if _, err := os.Stat("/usr/sbin/kexec"); err == nil { 243 | args = append(args, "-files=/usr/sbin/kexec") 244 | } 245 | 246 | if *wifi { 247 | args = append(args, 248 | "-files", extraBinMust("iwconfig"), 249 | "-files", extraBinMust("iwlist"), 250 | "-files", extraBinMust("wpa_supplicant")+":bin/wpa_supplicant", 251 | "-files", extraBinMust("wpa_cli")+":bin/wpa_cli", 252 | "-files", extraBinMust("wpa_passphrase")+":bin/wpa_passphrase", 253 | "-files", extraBinMust("strace"), 254 | "-files", "cmds/webboot/distros.json:distros.json", 255 | ) 256 | } 257 | if *bzImage != "" { 258 | args = append(args, "-files", *bzImage+":bzImage") 259 | } 260 | if *iso != "" { 261 | args = append(args, "-files", *iso+":iso") 262 | } 263 | args = append(args, "core", "./cmds/*") 264 | var commands = []cmd{ 265 | {args: append(append(args, strings.Fields(*uroot)...), *cmds)}, 266 | } 267 | 268 | for _, cmd := range commands { 269 | debug("Run %v", cmd) 270 | c := exec.Command(cmd.args[0], cmd.args[1:]...) 271 | c.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") 272 | c.Stdout, c.Stderr = os.Stdout, os.Stderr 273 | c.Dir = cmd.dir 274 | if err := c.Run(); err != nil { 275 | log.Fatalf("%s failed: %v", cmd, err) 276 | } 277 | } 278 | debug("done") 279 | } 280 | --------------------------------------------------------------------------------