From 858e14a5204ac8f31965259fe5f71c694b465176 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sat, 27 Sep 2025 16:42:47 +0200 Subject: [PATCH] Initial commit: Add NaaS (No as a Service) implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete NaaS HTTP service implementation with: - Pure Rust implementation using only standard library - Multiple response formats (text, JSON, XML, YAML, boolean) - Embedded web frontend with mobile-responsive design - Container support with Podman/Docker - Systemd service configuration - Health check endpoint - CORS support The service always responds with "no" in various formats, optimized for minimal binary size (~1MB) and fast response times. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cargo.lock | 7 + Cargo.toml | 13 ++ Containerfile | 56 ++++++++ naas.service | 50 +++++++ src/main.rs | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 498 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Containerfile create mode 100644 naas.service create mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2f87986 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "naas" +version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..15b3456 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "naas" +version = "1.0.0" +edition = "2021" + +[dependencies] + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true +panic = "abort" \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..0fc69ff --- /dev/null +++ b/Containerfile @@ -0,0 +1,56 @@ +# Build stage +FROM docker.io/rust:1-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache musl-dev + +WORKDIR /app + +# Copy manifests +COPY Cargo.toml ./ + +# Create dummy main.rs for dependency caching +RUN mkdir src && echo "fn main() {}" > src/main.rs + +# Build dependencies +RUN cargo build --release +RUN rm -rf src + +# Copy source code +COPY src ./src + +# Touch main.rs to ensure rebuild +RUN touch src/main.rs + +# Build the application +RUN cargo build --release + +# Runtime stage +FROM docker.io/alpine:latest + +# Install runtime dependencies +RUN apk add --no-cache ca-certificates + +# Create non-root user +RUN adduser -D -u 1000 naas + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /app/target/release/naas /app/naas +RUN chmod +x /app/naas + +# Change ownership +RUN chown -R naas:naas /app + +USER naas + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# Run the application +CMD ["./naas"] \ No newline at end of file diff --git a/naas.service b/naas.service new file mode 100644 index 0000000..b8d3182 --- /dev/null +++ b/naas.service @@ -0,0 +1,50 @@ +[Unit] +Description=No as a Service (NaaS) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +Restart=always +RestartSec=10 +TimeoutStopSec=30 + +# User configuration (adjust as needed) +User=naas +Group=naas + +# Environment configuration +Environment="PORT=8080" +Environment="BUILDAH_FORMAT=docker" + +# Container management +ExecStartPre=/usr/bin/podman rm -f naas-container 2>/dev/null || true +ExecStartPre=/usr/bin/podman build -t naas:latest /opt/naas + +ExecStart=/usr/bin/podman run \ + --name naas-container \ + --rm \ + --network=host \ + --read-only \ + --security-opt no-new-privileges \ + --cap-drop ALL \ + --cap-add NET_BIND_SERVICE \ + -e PORT=8080 \ + naas:latest + +ExecStop=/usr/bin/podman stop naas-container +ExecStopPost=/usr/bin/podman rm -f naas-container 2>/dev/null || true + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths= + +# Resource limits +MemoryLimit=256M +CPUQuota=50% + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8ff749a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,372 @@ +use std::io::prelude::*; +use std::net::{TcpListener, TcpStream}; +use std::thread; + +const HTML_FRONTEND: &str = r#" + + + + + No as a Service + + + +
+
+

No as a Service

+

The API that always says no

+
+ +
+
NO
+
+ +
+ + + + + + +
+ +
+
+ Click a button to test the API +
+
+ +
+

API Endpoints

+
    +
  • GET /api/no - Returns "no"
  • +
  • GET /api/no?format=json - Returns JSON
  • +
  • GET /api/no?format=bool - Returns boolean
  • +
  • GET /api/no?format=xml - Returns XML
  • +
  • GET /api/no?format=yaml - Returns YAML
  • +
  • GET /health - Health check
  • +
+
+
+ + + +"#; + +fn main() { + let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()); + let addr = format!("0.0.0.0:{}", port); + + let listener = TcpListener::bind(&addr).expect("Failed to bind to address"); + println!("No as a Service running on http://{}", addr); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(|| { + handle_connection(stream); + }); + } + Err(e) => { + eprintln!("Connection failed: {}", e); + } + } + } +} + +fn handle_connection(mut stream: TcpStream) { + let mut buffer = [0; 1024]; + if let Err(e) = stream.read(&mut buffer) { + eprintln!("Failed to read from stream: {}", e); + return; + } + + let request = String::from_utf8_lossy(&buffer[..]); + let request_line = request.lines().next().unwrap_or(""); + + let (status, content_type, body) = route_request(request_line); + + let response = format!( + "HTTP/1.1 {}\r\n\ + Content-Type: {}\r\n\ + Content-Length: {}\r\n\ + Access-Control-Allow-Origin: *\r\n\ + Access-Control-Allow-Methods: GET, OPTIONS\r\n\ + Access-Control-Allow-Headers: Content-Type\r\n\ + \r\n\ + {}", + status, + content_type, + body.len(), + body + ); + + if let Err(e) = stream.write_all(response.as_bytes()) { + eprintln!("Failed to write response: {}", e); + } + + if let Err(e) = stream.flush() { + eprintln!("Failed to flush stream: {}", e); + } +} + +fn route_request(request_line: &str) -> (&str, &str, String) { + let parts: Vec<&str> = request_line.split_whitespace().collect(); + if parts.len() < 2 { + return ("400 Bad Request", "text/plain", "Bad Request".to_string()); + } + + let method = parts[0]; + let path = parts[1]; + + if method == "OPTIONS" { + return ("200 OK", "text/plain", "".to_string()); + } + + if method != "GET" { + return ("405 Method Not Allowed", "text/plain", "Method Not Allowed".to_string()); + } + + match path { + "/" => ("200 OK", "text/html; charset=utf-8", HTML_FRONTEND.to_string()), + "/health" => ("200 OK", "text/plain", "OK".to_string()), + path if path.starts_with("/api/no") => handle_api_no(path), + _ => ("404 Not Found", "text/plain", "Not Found".to_string()), + } +} + +fn handle_api_no(path: &str) -> (&'static str, &'static str, String) { + let format = if let Some(query_start) = path.find('?') { + let query = &path[query_start + 1..]; + parse_query_param(query, "format") + } else { + None + }; + + match format.as_deref() { + Some("json") => ( + "200 OK", + "application/json", + r#"{"answer":"no"}"#.to_string() + ), + Some("bool") => ( + "200 OK", + "application/json", + "false".to_string() + ), + Some("xml") => ( + "200 OK", + "application/xml", + r#"no"#.to_string() + ), + Some("yaml") => ( + "200 OK", + "text/yaml", + "answer: no\n".to_string() + ), + _ => ( + "200 OK", + "text/plain", + "no".to_string() + ), + } +} + +fn parse_query_param(query: &str, param_name: &str) -> Option { + for pair in query.split('&') { + let mut parts = pair.split('='); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + if key == param_name { + return Some(value.to_string()); + } + } + } + None +} \ No newline at end of file