Browse Source

Added TFTP server

- Added basic TFTP server function
- Added user list API without icon
- Added more utils for GET parameters
Toby Chui 4 weeks ago
parent
commit
6fc5f9f27a

+ 95 - 0
src/mod/fileservers/servers/tftpserv/handler.go

@@ -0,0 +1,95 @@
+package tftpserv
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"imuslab.com/arozos/mod/utils"
+)
+
+// Start the TFTP Server by request
+func (m *Manager) HandleTFTPServerStart(w http.ResponseWriter, r *http.Request) {
+	err := m.StartTftpServer()
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+	utils.SendOK(w)
+}
+
+// Stop the TFTP server by request
+func (m *Manager) HandleTFTPServerStop(w http.ResponseWriter, r *http.Request) {
+	m.StopTftpServer()
+	utils.SendOK(w)
+}
+
+// Get the TFTP server status
+func (m *Manager) HandleTFTPServerStatus(w http.ResponseWriter, r *http.Request) {
+	status, err := m.GetTftpServerStatus()
+	if err != nil {
+		utils.SendErrorResponse(w, err.Error())
+		return
+	}
+
+	js, _ := json.Marshal(status)
+	utils.SendJSONResponse(w, string(js))
+}
+
+// Handle TFTP Server Port number change
+func (m *Manager) HandleTFTPPort(w http.ResponseWriter, r *http.Request) {
+	newport, _ := utils.GetPara(r, "port")
+	if newport == "" {
+		//Get current port
+		port := 69
+		if m.option.Sysdb.KeyExists("tftp", "port") {
+			m.option.Sysdb.Read("tftp", "port", &port)
+		}
+		js, _ := json.Marshal(port)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		//Set new port
+		newPortInt, err := strconv.Atoi(newport)
+		if err != nil {
+			utils.SendErrorResponse(w, "Invalid port number")
+			return
+		}
+
+		m.option.Sysdb.Write("tftp", "port", newPortInt)
+
+		//Restart the TFTP server if running
+		if m.IsTftpServerEnabled() {
+			m.StopTftpServer()
+			m.StartTftpServer()
+		}
+
+		utils.SendOK(w)
+	}
+}
+
+// Handle setting the default user for TFTP access
+func (m *Manager) HandleTFTPDefaultUser(w http.ResponseWriter, r *http.Request) {
+	username, _ := utils.PostPara(r, "username")
+	if r.Method == http.MethodGet || username == "" {
+		//Get current default user
+		defaultUser := ""
+		if m.option.Sysdb.KeyExists("tftp", "defaultUser") {
+			m.option.Sysdb.Read("tftp", "defaultUser", &defaultUser)
+		}
+		js, _ := json.Marshal(defaultUser)
+		utils.SendJSONResponse(w, string(js))
+	} else {
+		//Set new default user
+		// Validate that the user exists
+		_, err := m.option.UserManager.GetUserInfoFromUsername(username)
+		if err != nil {
+			utils.SendErrorResponse(w, "User not found")
+			return
+		}
+
+		m.option.Sysdb.Write("tftp", "defaultUser", username)
+		m.option.Logger.PrintAndLog("TFTP", "Default user set to: "+username, nil)
+
+		utils.SendOK(w)
+	}
+}

+ 150 - 0
src/mod/fileservers/servers/tftpserv/tftpserv.go

@@ -0,0 +1,150 @@
+package tftpserv
+
+import (
+	"imuslab.com/arozos/mod/database"
+	"imuslab.com/arozos/mod/fileservers"
+	"imuslab.com/arozos/mod/info/logger"
+	"imuslab.com/arozos/mod/storage/tftp"
+	user "imuslab.com/arozos/mod/user"
+)
+
+type ServerStatus struct {
+	Enabled     bool
+	Port        int
+	DefaultUser string
+	UserGroups  []string
+}
+
+type ManagerOption struct {
+	Hostname    string
+	TmpFolder   string
+	Logger      *logger.Logger
+	UserManager *user.UserHandler
+	TftpServer  *tftp.Handler
+	Sysdb       *database.Database
+}
+
+type Manager struct {
+	option ManagerOption
+}
+
+// Create a new TFTP Manager
+func NewTFTPManager(option *ManagerOption) *Manager {
+	//Create database related tables
+	option.Sysdb.NewTable("tftp")
+	defaultEnable := false
+	if option.Sysdb.KeyExists("tftp", "default") {
+		option.Sysdb.Read("tftp", "default", &defaultEnable)
+	} else {
+		option.Sysdb.Write("tftp", "default", false)
+	}
+
+	//Create the Manager object
+	manager := Manager{
+		option: *option,
+	}
+
+	//Enable this service
+	if defaultEnable {
+		manager.StartTftpServer()
+	}
+
+	return &manager
+}
+
+func (m *Manager) StartTftpServer() error {
+	if m.option.TftpServer != nil {
+		//If the previous tftp server is not closed, close it and open a new one
+		m.option.TftpServer.Close()
+	}
+
+	//Load new server config from database
+	serverPort := int(69)
+	if m.option.Sysdb.KeyExists("tftp", "port") {
+		m.option.Sysdb.Read("tftp", "port", &serverPort)
+	}
+
+	//Create a new TFTP Handler
+	h, err := tftp.NewTFTPHandler(m.option.UserManager, m.option.Hostname, serverPort, m.option.TmpFolder)
+	if err != nil {
+		return err
+	}
+	h.Start()
+	m.option.TftpServer = h
+
+	//Remember the TFTP server status
+	m.option.Sysdb.Write("tftp", "default", true)
+
+	return nil
+}
+
+func (m *Manager) StopTftpServer() {
+	if m.option.TftpServer != nil {
+		m.option.TftpServer.Close()
+	}
+
+	m.option.Sysdb.Write("tftp", "default", false)
+	m.option.Logger.PrintAndLog("TFTP", "TFTP Server Stopped", nil)
+}
+
+func (m *Manager) GetTftpServerStatus() (*ServerStatus, error) {
+	enabled := false
+	if m.option.TftpServer != nil && m.option.TftpServer.ServerRunning {
+		enabled = true
+	}
+
+	serverPort := 69
+	if m.option.Sysdb.KeyExists("tftp", "port") {
+		m.option.Sysdb.Read("tftp", "port", &serverPort)
+	}
+
+	userGroups := []string{}
+	if m.option.Sysdb.KeyExists("tftp", "groups") {
+		m.option.Sysdb.Read("tftp", "groups", &userGroups)
+	}
+
+	defaultUser := ""
+	if m.option.Sysdb.KeyExists("tftp", "defaultUser") {
+		m.option.Sysdb.Read("tftp", "defaultUser", &defaultUser)
+	}
+
+	currentStatus := ServerStatus{
+		Enabled:     enabled,
+		Port:        serverPort,
+		DefaultUser: defaultUser,
+		UserGroups:  userGroups,
+	}
+	return &currentStatus, nil
+}
+
+func (m *Manager) IsTftpServerEnabled() bool {
+	return m.option.TftpServer != nil && m.option.TftpServer.ServerRunning
+}
+
+func (m *Manager) TFTPServerToggle(enabled bool) error {
+	if m.option.TftpServer != nil && m.option.TftpServer.ServerRunning {
+		//Enabled
+		if !enabled {
+			//Shut it down
+			m.StopTftpServer()
+		}
+	} else if enabled {
+		//Startup TFTP Server
+		return m.StartTftpServer()
+	}
+	return nil
+}
+
+func (m *Manager) TFTPGetEndpoints(userinfo *user.User) []*fileservers.Endpoint {
+	tftpEndpoints := []*fileservers.Endpoint{}
+	port := 69
+	if m.option.TftpServer != nil {
+		port = m.option.TftpServer.Port
+	}
+	tftpEndpoints = append(tftpEndpoints, &fileservers.Endpoint{
+		ProtocolName: "tftp://",
+		Port:         port,
+		Subpath:      "",
+	})
+	return tftpEndpoints
+}

+ 257 - 0
src/mod/storage/tftp/aofs.go

@@ -0,0 +1,257 @@
+package tftp
+
+// arozos virtual path translation handler for TFTP
+// Similar to FTP aofs implementation but adapted for TFTP
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/spf13/afero"
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/user"
+)
+
+var (
+	aofsCanRead  = 1
+	aofsCanWrite = 2
+)
+
+type aofs struct {
+	userinfo  *user.User
+	tmpFolder string
+}
+
+func (a aofs) Create(name string) (afero.File, error) {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Create(rewritePath)
+}
+
+func (a aofs) Chown(name string, uid, gid int) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chown(rewritePath, uid, gid)
+}
+
+func (a aofs) Mkdir(name string, perm os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Mkdir(rewritePath, perm)
+}
+
+func (a aofs) MkdirAll(path string, perm os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.MkdirAll(rewritePath, perm)
+}
+
+func (a aofs) Open(name string) (afero.File, error) {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanRead) {
+		return nil, errors.New("Permission denied")
+	}
+
+	return fsh.FileSystemAbstraction.Open(rewritePath)
+}
+
+func (a aofs) Stat(name string) (os.FileInfo, error) {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanRead) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Stat(rewritePath)
+}
+
+func (a aofs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return nil, err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return nil, errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.OpenFile(rewritePath, flag, perm)
+}
+
+func (a aofs) AllocateSpace(size int) error {
+	if a.userinfo.StorageQuota.HaveSpace(int64(size)) {
+		return nil
+	}
+	return errors.New("Storage Quota Full")
+}
+
+func (a aofs) Remove(name string) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+
+	return fsh.FileSystemAbstraction.Remove(rewritePath)
+}
+
+func (a aofs) RemoveAll(path string) error {
+	fsh, rewritePath, err := a.pathRewrite(path)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.RemoveAll(rewritePath)
+}
+
+func (a aofs) Rename(oldname, newname string) error {
+	fshsrc, rewritePathsrc, err := a.pathRewrite(oldname)
+	if err != nil {
+		return err
+	}
+
+	fshdest, rewritePathdest, err := a.pathRewrite(newname)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fshsrc, rewritePathsrc, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	if !a.checkAllowAccess(fshdest, rewritePathdest, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+
+	if !fshdest.FileSystemAbstraction.FileExists(filepath.Dir(rewritePathdest)) {
+		fshdest.FileSystemAbstraction.MkdirAll(filepath.Dir(rewritePathdest), 0775)
+	}
+
+	if fshsrc.UUID == fshdest.UUID {
+		//Renaming in same fsh
+		return fshsrc.FileSystemAbstraction.Rename(rewritePathsrc, rewritePathdest)
+	} else {
+		//Cross fsh read write.
+		f, err := fshsrc.FileSystemAbstraction.ReadStream(rewritePathsrc)
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		err = fshdest.FileSystemAbstraction.WriteStream(rewritePathdest, f, 0775)
+		if err != nil {
+			return err
+		}
+
+		err = fshsrc.FileSystemAbstraction.RemoveAll(rewritePathsrc)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (a aofs) Name() string {
+	return "arozos virtualFS (TFTP)"
+}
+
+func (a aofs) Chmod(name string, mode os.FileMode) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chmod(rewritePath, mode)
+}
+
+func (a aofs) Chtimes(name string, atime time.Time, mtime time.Time) error {
+	fsh, rewritePath, err := a.pathRewrite(name)
+	if err != nil {
+		return err
+	}
+	if !a.checkAllowAccess(fsh, rewritePath, aofsCanWrite) {
+		return errors.New("Permission denied")
+	}
+	return fsh.FileSystemAbstraction.Chtimes(rewritePath, atime, mtime)
+}
+
+// arozos adaptive functions
+// This function rewrite the path from tftp representation to real filepath on disk
+func (a aofs) pathRewrite(path string) (*filesystem.FileSystemHandler, string, error) {
+	path = filepath.ToSlash(filepath.Clean(path))
+
+	if path == "/" {
+		//Roots. Return TFTP do not list directory error
+		return nil, "", errors.New("TFTP does not support directory listing. Use use a valid file path like user:/Desktop/foo.txt")
+	} else if len(path) > 0 {
+		//Rewrite the path for any alternative filepath
+		//Get the uuid of the filepath
+		path = strings.TrimPrefix(path, "/")
+		path = strings.TrimPrefix(path, "./") // Prevent path traversal
+		// Support both /user/Desktop/test.txt or user:/Desktop/test.txt
+		path = strings.Replace(path, ":/", "/", 1)
+		fmt.Println(path)
+		subpaths := strings.Split(path, "/")
+		fsHandlerUUID := subpaths[0]
+		remainingPaths := subpaths[1:]
+		fsh, err := a.userinfo.GetFileSystemHandlerFromVirtualPath(fsHandlerUUID + ":/")
+		if err != nil {
+			return nil, "", errors.New("File System Abstraction not found")
+		}
+
+		rpath, err := fsh.FileSystemAbstraction.VirtualPathToRealPath(fsh.UUID+":/"+strings.Join(remainingPaths, "/"), a.userinfo.Username)
+		if err != nil {
+			return nil, "", errors.New("File System Handler Hierarchy not supported by TFTP driver")
+		}
+		return fsh, rpath, nil
+	} else {
+		//fsh not found.
+		return nil, "", errors.New("Invalid path")
+	}
+}
+
+// Check if user has access to the given path, mode can be {read / write}
+func (a aofs) checkAllowAccess(fsh *filesystem.FileSystemHandler, path string, mode int) bool {
+	vpath, err := fsh.FileSystemAbstraction.RealPathToVirtualPath(path, a.userinfo.Username)
+	if err != nil {
+		return false
+	}
+
+	if mode == aofsCanRead {
+		return a.userinfo.CanRead(vpath)
+	} else if mode == aofsCanWrite {
+		return a.userinfo.CanWrite(vpath)
+	} else {
+		return false
+	}
+}

+ 187 - 0
src/mod/storage/tftp/tftp.go

@@ -0,0 +1,187 @@
+package tftp
+
+import (
+	"errors"
+	"io"
+	"log"
+	"strconv"
+	"sync"
+	"time"
+
+	tftplib "github.com/pin/tftp/v3"
+	"imuslab.com/arozos/mod/database"
+	"imuslab.com/arozos/mod/user"
+)
+
+const (
+	// Maximum file size allowed for TFTP transfer (32MB)
+	MAX_FILE_SIZE = 32 * 1024 * 1024
+)
+
+// Handler is the handler for the TFTP server defined in arozos
+type Handler struct {
+	ServerName    string
+	Port          int
+	ServerRunning bool
+	userHandler   *user.UserHandler
+	server        *tftplib.Server
+	cancelFunc    func()
+}
+
+type tftpDriver struct {
+	userHandler       *user.UserHandler
+	tmpFolder         string
+	connectedUserList *sync.Map
+	db                *database.Database
+}
+
+// NewTFTPHandler creates a new handler for TFTP Server
+func NewTFTPHandler(userHandler *user.UserHandler, ServerName string, Port int, tmpFolder string) (*Handler, error) {
+	//Create table for tftp if it doesn't exists
+	db := userHandler.GetDatabase()
+	db.NewTable("tftp")
+
+	driver := &tftpDriver{
+		userHandler:       userHandler,
+		tmpFolder:         tmpFolder,
+		connectedUserList: &sync.Map{},
+		db:                db,
+	}
+
+	// Create a new TFTP Server instance
+	server := tftplib.NewServer(driver.readHandler, driver.writeHandler)
+	server.SetTimeout(5 * time.Second)
+
+	return &Handler{
+		ServerName:    ServerName,
+		Port:          Port,
+		ServerRunning: false,
+		userHandler:   userHandler,
+		server:        server,
+	}, nil
+}
+
+// Start the TFTP Server
+func (h *Handler) Start() error {
+	if h.server != nil {
+		addr := ":" + strconv.Itoa(h.Port)
+		log.Println("TFTP Server Started, listening at: " + strconv.Itoa(h.Port))
+
+		go func() {
+			err := h.server.ListenAndServe(addr)
+			if err != nil {
+				log.Println("TFTP Server error:", err)
+			}
+		}()
+
+		h.ServerRunning = true
+		return nil
+	} else {
+		return errors.New("TFTP server not initiated")
+	}
+}
+
+// Close the TFTP Server
+func (h *Handler) Close() {
+	if h.server != nil {
+		h.server.Shutdown()
+		h.ServerRunning = false
+	}
+}
+
+// readHandler handles TFTP read requests (GET)
+func (d *tftpDriver) readHandler(filename string, rf io.ReaderFrom) error {
+	// Get default user for TFTP access
+	// Since TFTP doesn't have authentication, we use a configured default user
+	username := ""
+	if d.db.KeyExists("tftp", "defaultUser") {
+		d.db.Read("tftp", "defaultUser", &username)
+	}
+
+	if username == "" {
+		return errors.New("no default user configured for TFTP access")
+	}
+
+	// Get user info
+	userinfo, err := d.userHandler.GetUserInfoFromUsername(username)
+	if err != nil {
+		return errors.New("default user not found")
+	}
+
+	// Create arozfs adapter
+	afs := &aofs{
+		userinfo:  userinfo,
+		tmpFolder: d.tmpFolder,
+	}
+
+	// Open file for reading
+	file, err := afs.Open(filename)
+	if err != nil {
+		log.Printf("TFTP READ: Failed to open %s: %v", filename, err)
+		return err
+	}
+	defer file.Close()
+
+	// Get file info
+	fileInfo, err := file.Stat()
+	if err != nil {
+		return err
+	}
+
+	// TFTP has a size limit (typically for smaller files)
+	// We'll allow up to 32MB
+	if fileInfo.Size() > MAX_FILE_SIZE {
+		return errors.New("file too large for TFTP transfer")
+	}
+
+	n, err := rf.ReadFrom(file)
+	if err != nil {
+		log.Printf("TFTP READ: Transfer error for %s: %v", filename, err)
+		return err
+	}
+
+	log.Printf("TFTP READ: Sent %s (%d bytes)", filename, n)
+	return nil
+}
+
+// writeHandler handles TFTP write requests (PUT)
+func (d *tftpDriver) writeHandler(filename string, wt io.WriterTo) error {
+	// Get default user for TFTP access
+	username := ""
+	if d.db.KeyExists("tftp", "defaultUser") {
+		d.db.Read("tftp", "defaultUser", &username)
+	}
+
+	if username == "" {
+		return errors.New("no default user configured for TFTP access")
+	}
+
+	// Get user info
+	userinfo, err := d.userHandler.GetUserInfoFromUsername(username)
+	if err != nil {
+		return errors.New("default user not found")
+	}
+
+	// Create arozfs adapter
+	afs := &aofs{
+		userinfo:  userinfo,
+		tmpFolder: d.tmpFolder,
+	}
+
+	// Check if user can write
+	file, err := afs.Create(filename)
+	if err != nil {
+		log.Printf("TFTP WRITE: Failed to create %s: %v", filename, err)
+		return err
+	}
+	defer file.Close()
+
+	n, err := wt.WriteTo(file)
+	if err != nil {
+		log.Printf("TFTP WRITE: Transfer error for %s: %v", filename, err)
+		return err
+	}
+
+	log.Printf("TFTP WRITE: Received %s (%d bytes)", filename, n)
+	return nil
+}

+ 33 - 0
src/mod/utils/utils.go

@@ -51,6 +51,39 @@ func GetPara(r *http.Request, key string) (string, error) {
 	}
 }
 
+func GetBool(r *http.Request, key string) (bool, error) {
+	x, err := GetPara(r, key)
+	if err != nil {
+		return false, err
+	}
+
+	x = strings.TrimSpace(x)
+
+	if x == "1" || strings.ToLower(x) == "true" {
+		return true, nil
+	} else if x == "0" || strings.ToLower(x) == "false" {
+		return false, nil
+	}
+
+	return false, errors.New("invalid boolean given")
+}
+
+// Get GET paramter as int
+func GetInt(r *http.Request, key string) (int, error) {
+	x, err := GetPara(r, key)
+	if err != nil {
+		return 0, err
+	}
+
+	x = strings.TrimSpace(x)
+	rx, err := strconv.Atoi(x)
+	if err != nil {
+		return 0, err
+	}
+
+	return rx, nil
+}
+
 // Get POST paramter
 func PostPara(r *http.Request, key string) (string, error) {
 	r.ParseForm()

+ 5 - 5
src/module.util.go

@@ -53,19 +53,19 @@ func util_init() {
 	})
 
 	/*
-		STL File Viewer - Plotted from ArOZ Online Beta
+		3D Model Viewer - Supports STL and OBJ files
 	*/
 	moduleHandler.RegisterModule(module.ModuleInfo{
-		Name:         "STL Viewer",
-		Desc:         "3D Model Viewer for STL Files",
+		Name:         "3D Model Viewer",
+		Desc:         "3D Model Viewer for STL and OBJ Files",
 		Group:        "Utilities",
 		IconPath:     "SystemAO/utilities/img/stlViewer.png",
-		Version:      "1.0",
+		Version:      "2.0",
 		SupportFW:    false,
 		SupportEmb:   true,
 		LaunchEmb:    "SystemAO/utilities/stlViewer.html",
 		InitEmbSize:  []int{720, 500},
-		SupportedExt: []string{".stl"},
+		SupportedExt: []string{".stl", ".obj"},
 	})
 
 	/*

+ 32 - 0
src/network.go

@@ -12,6 +12,7 @@ import (
 	"imuslab.com/arozos/mod/fileservers/servers/ftpserv"
 	"imuslab.com/arozos/mod/fileservers/servers/samba"
 	"imuslab.com/arozos/mod/fileservers/servers/sftpserv"
+	"imuslab.com/arozos/mod/fileservers/servers/tftpserv"
 	"imuslab.com/arozos/mod/fileservers/servers/webdavserv"
 	network "imuslab.com/arozos/mod/network"
 	mdns "imuslab.com/arozos/mod/network/mdns"
@@ -33,6 +34,7 @@ var (
 
 	//File Server Managers
 	FTPManager        *ftpserv.Manager
+	TFTPManager       *tftpserv.Manager
 	WebDAVManager     *webdavserv.Manager
 	SFTPManager       *sftpserv.Manager
 	SambaShareManager *samba.ShareManager
@@ -305,6 +307,16 @@ func FileServerInit() {
 		AllowUpnp:   *allow_upnp,
 	})
 
+	//TFTP
+	TFTPManager = tftpserv.NewTFTPManager(&tftpserv.ManagerOption{
+		Hostname:    *host_name,
+		TmpFolder:   *tmp_directory,
+		Logger:      systemWideLogger,
+		UserManager: userHandler,
+		TftpServer:  nil,
+		Sysdb:       sysdb,
+	})
+
 	//SFTP
 	SFTPManager = sftpserv.NewSFTPServer(&sftpserv.ManagerOption{
 		Hostname:    *host_name,
@@ -355,6 +367,11 @@ func FileServerInit() {
 	adminRouter.HandleFunc("/system/storage/ftp/setPort", FTPManager.HandleFTPSetPort)
 	adminRouter.HandleFunc("/system/storage/ftp/passivemode", FTPManager.HandleFTPPassiveModeSettings)
 
+	//TFTP
+	adminRouter.HandleFunc("/system/storage/tftp/status", TFTPManager.HandleTFTPServerStatus)
+	adminRouter.HandleFunc("/system/storage/tftp/setPort", TFTPManager.HandleTFTPPort)
+	adminRouter.HandleFunc("/system/storage/tftp/defaultUser", TFTPManager.HandleTFTPDefaultUser)
+
 	//Samba Shares (Optional)
 	if SambaShareManager != nil {
 		//Activate and Deactivate are functions all users can use if admin enabled smbd service
@@ -429,6 +446,21 @@ func FileServerInit() {
 		GetEndpoints:      FTPManager.FTPGetEndpoints,
 	})
 
+	networkFileServerDaemon = append(networkFileServerDaemon, &fileservers.Server{
+		ID:                "tftp",
+		Name:              "TFTP",
+		Desc:              "Trivial File Transfer Protocol Server",
+		IconPath:          "img/system/network-folder.svg",
+		DefaultPorts:      []int{69},
+		Ports:             []int{},
+		ForwardPortIfUpnp: false,
+		ConnInstrPage:     "SystemAO/disk/instr/tftp.html",
+		ConfigPage:        "SystemAO/disk/tftp.html",
+		EnableCheck:       TFTPManager.IsTftpServerEnabled,
+		ToggleFunc:        TFTPManager.TFTPServerToggle,
+		GetEndpoints:      TFTPManager.TFTPGetEndpoints,
+	})
+
 	networkFileServerDaemon = append(networkFileServerDaemon, &fileservers.Server{
 		ID:                "dirserv",
 		Name:              "Directory Server",

+ 20 - 0
src/system/tftp/README.txt

@@ -0,0 +1,20 @@
+==========================================
+    AROZOS TFTP SERVER ROOT DIRECTORY
+==========================================
+
+TFTP (Trivial File Transfer Protocol) is a simple file transfer protocol.
+
+IMPORTANT NOTES:
+1. DO NOT upload files directly to this root directory
+2. Navigate to the filesystem UUID subdirectories to access your files
+3. TFTP does not support directory listing
+4. TFTP is designed for small file transfers (max 32MB per file)
+5. Files must be referenced by their full path including UUID
+
+SECURITY NOTICE:
+- TFTP has no built-in authentication
+- Access is controlled via the configured default user
+- User group permissions apply to all transfers
+- Consider using SFTP or FTPS for sensitive data
+
+For more information, visit the ArozOS documentation.

+ 6 - 1
src/user.go

@@ -446,6 +446,8 @@ func user_handleList(w http.ResponseWriter, r *http.Request) {
 		utils.SendErrorResponse(w, "User not logged in")
 		return
 	}
+
+	noicon, _ := utils.GetBool(r, "noicon")
 	if authAgent.CheckAuth(r) {
 		entries, _ := sysdb.ListTable("auth")
 		var results [][]interface{}
@@ -454,7 +456,10 @@ func user_handleList(w http.ResponseWriter, r *http.Request) {
 				username := strings.Split(string(keypairs[0]), "/")[1]
 				group := []string{}
 				//Get user icon if it exists in the database
-				userIcon := getUserIcon(username)
+				userIcon := ""
+				if !noicon {
+					userIcon = getUserIcon(username)
+				}
 
 				json.Unmarshal(keypairs[1], &group)
 				var thisUserInfo []interface{}

+ 98 - 0
src/web/SystemAO/disk/instr/tftp.html

@@ -0,0 +1,98 @@
+<div class="ui yellow message" style="margin-top: 0;">
+    <h4 class="ui header">
+        <i class="warning icon"></i>
+        <div class="content">
+            TFTP Protocol Notice
+        </div>
+    </h4>
+    <p><b>TFTP (Trivial File Transfer Protocol)</b> is a simple, lightweight file transfer protocol designed for basic file transfer operations.</p>
+    <ul class="ui list">
+        <li><b>No Authentication:</b> TFTP does not support user authentication. Access control is managed through the default user configuration.</li>
+        <li><b>No Directory Listing:</b> TFTP clients cannot browse directories. You must know the exact file path.</li>
+        <li><b>File Size Limit:</b> Maximum file size is 32MB per transfer.</li>
+        <li><b>UDP Protocol:</b> TFTP uses UDP (port 69 by default), which may require firewall configuration.</li>
+        <li><b>Security Warning:</b> Use TFTP only in trusted networks. For secure file transfer, consider using SFTP instead.</li>
+    </ul>
+</div>
+
+<div class="ui styled fluid accordion" style="margin-top: 1em;">
+    <div class="active  title" >
+        <i class="dropdown icon"></i>
+        <i class="windows icon"></i>
+        Using TFTP on Windows
+    </div>
+    <div class="active  content">
+        <p>Windows includes a built-in TFTP client. First, enable it via Control Panel:</p>
+        <ol>
+            <li>Open <code>Control Panel</code> → <code>Programs and Features</code></li>
+            <li>Click <code>Turn Windows features on or off</code></li>
+            <li>Check <b>TFTP Client</b> and click OK</li>
+        </ol>
+        <p>To download a file from ArozOS TFTP server, open Command Prompt and run:</p>
+        <code>tftp -i [hostname] GET [filesystem-uuid]/path/to/file.txt C:\destination\file.txt</code>
+        <br><br>
+        <p>To upload a file to ArozOS TFTP server:</p>
+        <code>tftp -i [hostname] PUT C:\source\file.txt [filesystem-uuid]/path/to/file.txt</code>
+    </div>
+    
+    <div class="title maconly">
+        <i class="dropdown icon"></i>
+        <i class="apple icon"></i>
+        Using TFTP on macOS
+    </div>
+    <div class="content maconly">
+        <p>macOS includes a built-in TFTP client accessible via Terminal.</p>
+        <p>To download a file from ArozOS TFTP server, open Terminal and run:</p>
+        <code>tftp [hostname]</code><br>
+        <code>get [filesystem-uuid]/path/to/file.txt /destination/path/file.txt</code><br>
+        <code>quit</code>
+        <br><br>
+        <p>To upload a file to ArozOS TFTP server:</p>
+        <code>tftp [hostname]</code><br>
+        <code>put /source/path/file.txt [filesystem-uuid]/path/to/file.txt</code><br>
+        <code>quit</code>
+    </div>
+    
+    <div class="title">
+        <i class="dropdown icon"></i>
+        <i class="linux icon"></i>
+        Using TFTP on Linux
+    </div>
+    <div class="content">
+        <p>Install TFTP client if not already installed:</p>
+        <code>sudo apt-get install tftp-hpa</code> (Debian/Ubuntu)<br>
+        <code>sudo yum install tftp</code> (RedHat/CentOS)
+        <br><br>
+        <p>To transfer files:</p>
+        <code>tftp [hostname]</code><br>
+        <code>get [filesystem-uuid]/path/to/file.txt</code><br>
+        <code>put /local/file.txt [filesystem-uuid]/path/to/file.txt</code><br>
+        <code>quit</code>
+    </div>
+    
+    <div class="title">
+        <i class="dropdown icon"></i>
+        <i class="info circle icon"></i>
+        File System UUID Path Format
+    </div>
+    <div class="content">
+        <p>TFTP file paths in ArozOS follow this format:</p>
+        <code>[filesystem-uuid]/path/to/file</code>
+        <br><br>
+        <p>To find your filesystem UUID, check your File Manager or system settings. Common UUIDs include:</p>
+        <ul>
+            <li><code>user</code> - Your personal user directory</li>
+            <li><code>tmp</code> - Temporary files directory</li>
+        </ul>
+        <p><b>You can use either:</b> <code>user:/documents/report.txt</code> or <code>/user/documents/report.txt</code> in TFTP</p>
+    </div>
+</div>
+
+<script>
+    var isMac = navigator.platform.indexOf('Mac') > -1;
+    var isWindows = navigator.platform.indexOf('Win') > -1;
+
+
+    // Initialize Semantic UI accordion
+    $('.ui.accordion').accordion();
+</script>

+ 1 - 10
src/web/SystemAO/disk/services.html

@@ -73,16 +73,7 @@
             <div class="six wide column" style="border-right: 1px solid #e0e0e0;">
                 <div id="serviceList"></div>
                 <div class="ui divider"></div>
-                <!-- 
-                <div style="width: 100%;" align="center">
-                    <button title="Start All Services" onclick="" class="circular basic green large ui icon button">
-                        <i class="green play icon"></i>
-                    </button>
-                    <button title="Stop All Services" onclick="" class="circular basic red large ui icon button">
-                        <i class="red stop icon"></i>
-                    </button>
-                </div>
-                -->
+             
             </div>
             <div class="ten wide column">
                 <div id="serviceInstruction"></div>

+ 169 - 0
src/web/SystemAO/disk/tftp.html

@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        .hidden{
+            display:none;
+        }
+
+        .disabled{
+            opacity: 0.5;
+            pointer-events: none;
+        }
+    </style>
+</head>
+<body>
+    <div class="ui form">
+        <div class="field">
+            <label>Default User for TFTP Access</label>
+            <select id="defaultUser" class="ui search dropdown" >
+                <option value="">Select a user account</option>
+            </select>
+            <small>All TFTP file operations will be performed using this user's permissions and storage quota.</small>
+        </div>
+
+        <div class="field">
+            <label>Listening Port</label>
+            <div class="ui labeled input">
+                <input id="listeningPort" type="number" placeholder="69" min="69" onchange="updateTFTPPort(this.value)">
+            </div>
+            <small>Default TFTP port is 69. Changing this port requires TFTP clients to explicitly specify the port.</small>
+        </div>
+        <br><br>
+        <div id="ok" class="ui secondary inverted green segment" style="display:none;">
+            <i class="checkmark icon"></i> Setting Applied
+        </div>
+        <div id="error" class="ui secondary inverted red segment" style="display:none;">
+            <i class="remove icon"></i> <span class="msg">Something went wrong</span>
+        </div>
+    </div>
+    <br><br>
+    <script>
+        $(".ui.dropdown").dropdown();
+        $(document).ready(function(){
+            //Load user list for default user selection
+            $.get("../../system/users/list?noicon=true", function(data){
+                if (data.error !== undefined){
+                    console.log(data.error);
+                }else{
+                    data.forEach(user => {
+                        let username = user[0];
+                        $("#defaultUser").append(`<option value="${username}">${username}</option>`);
+                    });
+                }
+                //Select the current default user
+                initDefaultTFTPUsername(function(){
+                    $("#defaultUser").on('change', function(){
+                        updateDefaultUser(this.value);
+                    });
+                   
+                });
+                
+            });
+
+
+        });
+
+        function initTFTPServerStatus(){
+            //Load current system status
+            $.get("../../system/storage/tftp/status", function(data){
+                if (data.error !== undefined){
+                    console.log(data.error);
+                }else{
+                    if (data.Enabled == false){
+                        $("#listeningPort").parent().addClass("disabled");
+                    }
+
+                    $("#listeningPort").val(data.Port);
+                    $(".port").text(data.Port);
+
+                    //Set default user
+                    if (data.DefaultUser && data.DefaultUser != ""){
+                        $("#defaultUser").dropdown("set selected", data.DefaultUser);
+                    }
+
+                    //Set user groups
+                    if (data.UserGroups && data.UserGroups.length > 0){
+                        $("#grouplist").dropdown("set selected", data.UserGroups);
+                    }
+                }
+                $(".ui.checkbox").checkbox();
+            });
+        }
+
+        function showOk(){
+            $("#ok").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
+        }
+
+        function showError(msg){
+            $("#error .msg").text(msg);
+            $("#error").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
+        }
+
+        function updateDefaultUser(username){
+            if (username == ""){
+                showError("Please select a valid user");
+                return;
+            }
+
+            $.ajax({
+                url: "../../system/storage/tftp/defaultUser",
+                method: "POST",
+                data: {username: username},
+                success: function(data){
+                    if (data.error !== undefined){
+                        showError(data.error);
+                    }else{
+                        showOk();
+                    }
+                },
+                error: function(){
+                    showError("Failed to update default user");
+                }
+            });
+        }
+
+        function updateTFTPPort(port){
+            if (port < 1 || port > 65535){
+                showError("Invalid port number");
+                return;
+            }
+
+            $.ajax({
+                url: "../../system/storage/tftp/setPort",
+                method: "GET",
+                data: {port: port},
+                success: function(data){
+                    if (data.error !== undefined){
+                        showError(data.error);
+                    }else{
+                        showOk();
+                        //Reload status to reflect changes
+                        setTimeout(initTFTPServerStatus, 500);
+                    }
+                },
+                error: function(){
+                    showError("Failed to update port");
+                }
+            });
+        }
+
+        function initDefaultTFTPUsername(callback){
+            $.get("../../system/storage/tftp/defaultUser", function(data){
+                if (data.error !== undefined){
+                    console.log(data.error);
+                }else{
+                    if (data != ""){
+                        $("#defaultUser").dropdown("set selected", data);
+                    }
+                }
+
+                if (callback){
+                    callback();
+                }
+
+            });
+        }
+    </script>
+</body>
+</html>

+ 4 - 1
src/web/SystemAO/info/gomod-license.csv

@@ -50,6 +50,7 @@ github.com/nwaples/rardecode,https://github.com/nwaples/rardecode/blob/v1.1.3/LI
 github.com/oliamb/cutter,https://github.com/oliamb/cutter/blob/v0.2.2/LICENSE,MIT
 github.com/oov/psd,https://github.com/oov/psd/blob/5db5eafcecbb/LICENSE,MIT
 github.com/pierrec/lz4/v4,https://github.com/pierrec/lz4/blob/v4.1.22/LICENSE,BSD-3-Clause
+github.com/pin/tftp/v3,https://github.com/pin/tftp/blob/v3.1.0/LICENSE,MIT
 github.com/pjbgf/sha1cd,https://github.com/pjbgf/sha1cd/blob/v0.5.0/LICENSE,Apache-2.0
 github.com/pkg/sftp,https://github.com/pkg/sftp/blob/v1.13.9/LICENSE,BSD-2-Clause
 github.com/robertkrimen/otto,https://github.com/robertkrimen/otto/blob/v0.5.1/LICENSE,MIT
@@ -68,7 +69,7 @@ gitlab.com/NebulousLabs/fastrand,https://gitlab.com/NebulousLabs/fastrand/blob/6
 gitlab.com/NebulousLabs/go-upnp,https://gitlab.com/NebulousLabs/go-upnp/blob/11da932010b6/LICENSE,MIT
 gitlab.com/NebulousLabs/go-upnp/goupnp,https://gitlab.com/NebulousLabs/go-upnp/blob/11da932010b6/goupnp\LICENSE,BSD-2-Clause
 golang.org/x/crypto,https://cs.opensource.google/go/x/crypto/+/v0.44.0:LICENSE,BSD-3-Clause
-golang.org/x/image,https://cs.opensource.google/go/x/image/+/v0.32.0:LICENSE,BSD-3-Clause
+golang.org/x/image,https://cs.opensource.google/go/x/image/+/v0.33.0:LICENSE,BSD-3-Clause
 golang.org/x/net,https://cs.opensource.google/go/x/net/+/v0.47.0:LICENSE,BSD-3-Clause
 golang.org/x/oauth2,https://cs.opensource.google/go/x/oauth2/+/v0.32.0:LICENSE,BSD-3-Clause
 golang.org/x/sync/syncmap,https://cs.opensource.google/go/x/sync/+/v0.18.0:LICENSE,BSD-3-Clause
@@ -111,6 +112,7 @@ imuslab.com/arozos/mod/fileservers/servers/dirserv,Unknown,Unknown
 imuslab.com/arozos/mod/fileservers/servers/ftpserv,Unknown,Unknown
 imuslab.com/arozos/mod/fileservers/servers/samba,Unknown,Unknown
 imuslab.com/arozos/mod/fileservers/servers/sftpserv,Unknown,Unknown
+imuslab.com/arozos/mod/fileservers/servers/tftpserv,Unknown,Unknown
 imuslab.com/arozos/mod/fileservers/servers/webdavserv,Unknown,Unknown
 imuslab.com/arozos/mod/filesystem,Unknown,Unknown
 imuslab.com/arozos/mod/filesystem/abstractions/ftpfs,Unknown,Unknown
@@ -163,6 +165,7 @@ imuslab.com/arozos/mod/storage/bridge,Unknown,Unknown
 imuslab.com/arozos/mod/storage/du,Unknown,Unknown
 imuslab.com/arozos/mod/storage/ftp,Unknown,Unknown
 imuslab.com/arozos/mod/storage/sftpserver,Unknown,Unknown
+imuslab.com/arozos/mod/storage/tftp,Unknown,Unknown
 imuslab.com/arozos/mod/storage/webdav,Unknown,Unknown
 imuslab.com/arozos/mod/subservice,Unknown,Unknown
 imuslab.com/arozos/mod/time/nightly,Unknown,Unknown