Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http_server: serve license files #76

Merged
merged 8 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/tacd-webui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,17 @@ jobs:
node-version: latest
- run: npm ci
- run: npm run build

test:
name: npm run test
runs-on: ubuntu-latest
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: npm ci
- run: npm run test
9 changes: 9 additions & 0 deletions demo_files/usr/share/common-licenses/license.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
PACKAGE NAME: tacd
PACKAGE VERSION: 0.1.0+gitAUTOINC+803b2084b2
RECIPE NAME: tacd
LICENSE: GPL-2.0-or-later

PACKAGE NAME: tacd-webinterface
PACKAGE VERSION: 0.1.0+gitAUTOINC+803b2084b2
RECIPE NAME: tacd-webinterface
LICENSE: GPL-2.0-or-later
339 changes: 339 additions & 0 deletions demo_files/usr/share/licenses/tacd-webinterface/LICENSE

Large diffs are not rendered by default.

339 changes: 339 additions & 0 deletions demo_files/usr/share/licenses/tacd/LICENSE

Large diffs are not rendered by default.

26 changes: 21 additions & 5 deletions src/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ use serve_dir::serve_dir;
#[cfg(feature = "demo_mode")]
mod consts {
pub const WEBUI_DIR: &str = "web/build";
pub const LICENSE_DIR: &str = "demo_files/usr/share/licenses";
pub const LICENSE_MANIFEST: &str = "demo_files/usr/share/common-licenses/license.manifest";
pub const EXTRA_DIR: &str = "demo_files/srv/www";
pub const FS_PREFIX: &str = "demo_files";
pub const FALLBACK_PORT: &str = "[::]:8080";
Expand All @@ -37,12 +39,14 @@ mod consts {
#[cfg(not(feature = "demo_mode"))]
mod consts {
pub const WEBUI_DIR: &str = "/usr/share/tacd/webui";
pub const LICENSE_DIR: &str = "/usr/share/licenses";
pub const LICENSE_MANIFEST: &str = "/usr/share/common-licenses/license.manifest";
pub const EXTRA_DIR: &str = "/srv/www";
pub const FS_PREFIX: &str = "";
pub const FALLBACK_PORT: &str = "[::]:80";
}

use consts::{EXTRA_DIR, FALLBACK_PORT, FS_PREFIX, WEBUI_DIR};
use consts::{EXTRA_DIR, FALLBACK_PORT, FS_PREFIX, LICENSE_DIR, LICENSE_MANIFEST, WEBUI_DIR};

// openapi.json is generated by build.rs from openapi.yaml
const OPENAPI_JSON: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/openapi.json"));
Expand Down Expand Up @@ -78,8 +82,14 @@ impl HttpServer {
);

this.expose_openapi_json();
this.expose_dir(WEBUI_DIR, "/", false);
this.expose_dir(EXTRA_DIR, "/srv", true);
this.expose_dir(WEBUI_DIR, "/", false, None);
this.expose_dir(EXTRA_DIR, "/srv", true, None);
this.expose_dir(LICENSE_DIR, "/docs/legal/files", true, Some("text/plain"));

this.server
.at("/docs/legal/license.manifest")
.serve_file(LICENSE_MANIFEST)
.unwrap();

for (fs_path, web_path) in EXPOSED_FILES_RW {
let fs_path = FS_PREFIX.to_owned() + *fs_path;
Expand All @@ -103,8 +113,14 @@ impl HttpServer {
}

/// Serve a directory from disk for reading
fn expose_dir(&mut self, fs_path: &'static str, web_path: &str, directory_listings: bool) {
let handler = move |req| async move { serve_dir(fs_path, directory_listings, req).await };
fn expose_dir(
&mut self,
fs_path: &'static str,
web_path: &str,
directory_listings: bool,
force_mime: Option<&'static str>,
) {
let handler = move |req| serve_dir(req, fs_path, directory_listings, force_mime);

self.server.at(web_path).get(handler);
self.server.at(web_path).at("").get(handler);
Expand Down
19 changes: 14 additions & 5 deletions src/http_server/serve_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn clamp_timestamp(ts: SystemTime) -> SystemTime {
max(tacd_build_time, ts)
}

async fn file(req: &Request<()>, fs_path: &Path) -> Result {
async fn file(req: &Request<()>, fs_path: &Path, force_mime: Option<&str>) -> Result {
// Check the files modification date and compare it to the one provided
// by the client (if any) to determine if we even need to send the file.
let modification_date = fs_path.metadata()?.modified()?;
Expand Down Expand Up @@ -109,7 +109,11 @@ async fn file(req: &Request<()>, fs_path: &Path) -> Result {
.header("Last-Modified", last_modified)
.header("Cache-Control", "max-age=30, must-revalidate");

let body = Body::from_file(fs_path).await?;
let mut body = Body::from_file(fs_path).await?;

if let Some(mime) = force_mime {
body.set_mime(mime);
}

if have_gz && accept_gz {
let mut gz_body = Body::from_file(gz_path.unwrap()).await?;
Expand Down Expand Up @@ -241,7 +245,12 @@ fn dir_listing(fs_path: &Path, is_root: bool) -> Result {
Ok(res)
}

pub async fn serve_dir(base_path: &str, directory_listings: bool, req: Request<()>) -> Result {
pub async fn serve_dir(
req: Request<()>,
base_path: &str,
directory_listings: bool,
force_mime: Option<&str>,
) -> Result {
let url_path = req.url().path();
let has_trailing_slash = url_path.ends_with('/');

Expand Down Expand Up @@ -274,13 +283,13 @@ pub async fn serve_dir(base_path: &str, directory_listings: bool, req: Request<(

let res = {
if !is_dir {
file(&req, &path).await
file(&req, &path, force_mime).await
} else if !has_trailing_slash {
redirect_dir(url_path)
} else if directory_listings && !has_index {
dir_listing(&path, is_root)
} else {
file(&req, &index_path).await
file(&req, &index_path, force_mime).await
}
};

Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!@cloudscape-design)/\"",
"eject": "react-scripts eject"
},
"eslintConfig": {
Expand Down
5 changes: 4 additions & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ function Navigation() {
{
type: "section",
text: "Documentation",
items: [{ type: "link", text: "REST API", href: "#/docs/api" }],
items: [
{ type: "link", text: "REST API", href: "#/docs/api" },
{ type: "link", text: "Legal Information", href: "#/docs/legal" },
],
},
{
type: "section",
Expand Down
5 changes: 5 additions & 0 deletions web/src/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ export default function LandingPage() {
href: "/#/docs/api",
description: "Find API definitions to automate you LXA TAC",
},
{
name: "Documentation / Legal Information",
href: "/#/docs/legal",
description: "See the software components and their licenses",
},
]}
/>
</SpaceBetween>
Expand Down
39 changes: 39 additions & 0 deletions web/src/Legal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from "node:fs";
import ReactDOM from "react-dom/client";

import { parse_manifest, package_table } from "./Legal";

const manifest_ref = [
{
package_name: "tacd",
version: "0.1.0+gitAUTOINC+803b2084b2",
recipe_name: "tacd",
license: "GPL-2.0-or-later",
},
{
package_name: "tacd-webinterface",
version: "0.1.0+gitAUTOINC+803b2084b2",
recipe_name: "tacd-webinterface",
license: "GPL-2.0-or-later",
},
];

it("parses the manifest", () => {
const manifest_raw = fs.readFileSync(
"../demo_files/usr/share/common-licenses/license.manifest",
"utf-8",
);

const manifest = parse_manifest(manifest_raw);

expect(manifest).toEqual(manifest_ref);
});

it("renders", () => {
const div = document.createElement("div");
const root = ReactDOM.createRoot(div);

const manifest_table = package_table(manifest_ref);

root.render(manifest_table);
});
185 changes: 185 additions & 0 deletions web/src/Legal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// This file is part of tacd, the LXA TAC system daemon
// Copyright (C) 2024 Pengutronix e.K.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import Container from "@cloudscape-design/components/container";
import Header from "@cloudscape-design/components/header";
import Table from "@cloudscape-design/components/table";
import Link from "@cloudscape-design/components/link";
import SpaceBetween from "@cloudscape-design/components/space-between";

import { useEffect, useState } from "react";

type Package = {
package_name: string;
version: string;
recipe_name: string;
license: string;
};

export function parse_manifest(text: string) {
let packages: Package[] = [];

// The content of `text` looks something like this:
//
// PACKAGE NAME: tacd
// PACKAGE VERSION: 1.0.0
// RECIPE NAME: tacd
// LICENSE: GPL-2.0-or-later
Comment on lines +38 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How stable is the format generated by poky/meta/classes-recipe/license_image.bbclass? How hard would it be to have some kind of unit-test against a recent license file?

//
// PACKAGE NAME: tacd-webinterface
// ...

for (var group of text.split("\n\n")) {
let pkg: Package = {
package_name: "",
version: "",
recipe_name: "",
license: "",
};

for (var line of group.split("\n")) {
if (line.startsWith("PACKAGE NAME: ")) {
pkg.package_name = line.replace("PACKAGE NAME: ", "");
}
if (line.startsWith("PACKAGE VERSION: ")) {
pkg.version = line.replace("PACKAGE VERSION: ", "");
}
if (line.startsWith("RECIPE NAME: ")) {
pkg.recipe_name = line.replace("RECIPE NAME: ", "");
}
if (line.startsWith("LICENSE: ")) {
pkg.license = line.replace("LICENSE: ", "");
}
}

if (pkg.package_name && pkg.version && pkg.recipe_name && pkg.license) {
packages.push(pkg);
}
}

return packages;
}

export function package_table(packages?: Package[]) {
return (
<Table
header={
<Header
variant="h3"
description="Software packages used on this LXA TAC"
>
Packages
</Header>
}
columnDefinitions={[
{
id: "package_name",
header: "Package Name",
cell: (p) => p.package_name,
},
{
id: "version",
header: "Version",
cell: (p) => p.version,
},
{
id: "recipe_name",
header: "Recipe Name",
cell: (p) => p.recipe_name,
},
{
id: "license",
header: "License",
cell: (p) => (
<Link href={"/docs/legal/files/" + p.recipe_name}>{p.license}</Link>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I first thought that we could just set the mime type in the link on the front end, but we also generate a file listing. So you're approach is probably a good abstraction.

),
},
]}
items={packages || []}
loading={packages === undefined}
sortingDisabled
resizableColumns
stickyHeader
trackBy="package_name"
/>
);
}

function PackageList() {
const [packages, setPackages] = useState<Package[]>();

useEffect(() => {
fetch("/docs/legal/license.manifest").then((response) => {
if (response.ok) {
response.text().then((text) => setPackages(parse_manifest(text)));
}
});
}, []);

return package_table(packages);
}

export default function Legal() {
return (
<SpaceBetween size="m">
<Header
variant="h1"
description="Information regarding your rights as an LXA TAC software user"
>
LXA TAC / Legal Information
</Header>

<Container
header={
<Header
variant="h2"
description="Where to find the source code that makes up the LXA TAC software"
>
Availability of Source Code
</Header>
}
>
<p>
The LXA TAC software uses many pieces of free and open source
software. A list of these pieces of software, along with their version
number and their respective software license, is shown below.
</p>

<p>
Linux Automation GmbH provides all software components required to
build your own LXA TAC software bundles in the form of a public Yocto
Layer:{" "}
<Link href="https://github.com/linux-automation/meta-lxatac">
linux-automation/meta-lxatac
</Link>
.
</p>

<p>
To comply with the terms of copyleft licenses like the GPL we also
provide copies of their sources, along with the applied patches on our{" "}
<Link href="https://downloads.linux-automation.com/lxatac/software/">
download server
</Link>
.
</p>
</Container>

<PackageList />
</SpaceBetween>
);
}
Loading
Loading