Create the User
adduser username
Set User Password
passwd username
Give User SUDO Privilages
usermod -aG wheel username
Log in as User
su – username
Generate SSH key
ssh-keygen -t rsa -b 4096 -C "me@myemail.com"
adduser username
passwd username
usermod -aG wheel username
su – username
ssh-keygen -t rsa -b 4096 -C "me@myemail.com"
Every UI Document component references a UI Document asset (.uxml
file) that defines the UI and a Panel Settings asset that renders it. You can connect more than one UI Document asset to a single Panel Settings asset.
Right-click Project window, and then select Create > UI Toolkit > UI Document
MonoBehaviour
.Any UXML file in the project is available as a library object in the UI Builder.
Library→Project→Component (RMB)→Open UI Builder
Close the current context and open the component in the UI Builder.
Hierarchy→Component (RMB)→Open Instance in Isolation
Edit the component in the UI Builder, maintaining a breadcrumb to the previous context.
Hierarchy→Component (RMB)→Open Instance in Context
Edit the component while keeping the view on the current component. Maintains a breadcrumb to the previous context.
Typically a controller for a UI Element will is added to the UI Document as a component. Alternately the the UI Document can be set as a field on a MonoBehaviour instance.
Extending the MonoBehaviour class is most expedient way to control a UI component when it has only one instance. Public accessors permit access from other controllers, and should be used sparingly.
Select UI Document, in the Inspector→Add Component→Scripts.
using UnityEngine;
using UnityEngine.UIElements;
public class GameListController : MonoBehaviour {
public string GameListID = "GameList";
private VisualElement Root {
get { return GetComponent<UIDocument>().rootVisualElement; }
}
private ListView GameList{
get { return Root.Q<ListView>(GameListID); }
}
}
Add event handlers to the MonoBehaviour Start method.
public void Start() {
Root.Q<Button>(JoinButtonID).RegisterCallback<ClickEvent>(ClickJoinButton);
}
public UIDocument uiDocument1;
[SerializeReference] private UIDocument uiDocument2;
var uiDocument = GetComponent<UIDocument>();
public VisualElement Root {
get { return GetComponent<UIDocument>().rootVisualElement; }
}
Create a C# class derived from the VisualElement
class or a subclass.
You can initialize the control in the constructor, or when it’s added to the UI.
https://docs.unity3d.com/Manual/UIE-Events.html
You can register an event handler on an existing class, to handle events such as a mouse click.
ScrollView scrollView = Root.Q<ScrollView>(GameListID);
scrollView.contentContainer.Add(new Label("I done got clicked"));
Override VisualElement.ContainsPoint()
to assign custom intersection logic. [source]
You can also create USS custom properties to style a custom control. [source]
https://docs.unity3d.com/2022.2/Documentation/Manual/UIToolkits.html
https://docs.unity3d.com/2022.2/Documentation/Manual/UIE-get-started-with-runtime-ui.html
https://docs.unity3d.com/ScriptReference/UIElements.VisualElement.html
https://docs.unity3d.com/Manual/UIE-USS.html
https://docs.unity3d.com/Manual/UIE-create-tabbed-menu-for-runtime.html
NetCat (nc) is used to read and write to TCP and UDP network connections. It is an excellent tool for diagnosing network connectivity issues.
sudo apt-get install netcat
To listen to a specific port.
nc -l -p 8000
To write to a specific port
nc 127.0.0.1 8000
nc -v 127.0.0.1 8000
Database access is a core aspect of many applications. C# can interface with many different databases such as Oracle, Microsoft SQL, and MongoDB. In this case, we will be working with MySQL. The first step is to add the MySQL package to your project.
dotnet add package MySql.Data
When you run a client program to connect to the MySQL server, it is inadvisable to specify your password in a way that exposes it to discovery by other users. We will store our credentials in a .env file and load them into the environment variables. The code to load .env files is trivial, so we will just add it directly to the project. Add the .env file to your .gitignore so that you don’t upload your credentials to your code repository.
class DotEnv {
public static void Load() {
string filepath = Path.Combine(
Directory.GetCurrentDirectory(),
".env"
);
DotEnv.Load(filepath);
}
public static void Load(string filePath) {
if (!File.Exists(filePath)) return;
foreach (var line in File.ReadAllLines(filePath)) {
var parts = line.Split('=',StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2) continue;
if (Environment.GetEnvironmentVariable(parts[0]) != null) continue;
Environment.SetEnvironmentVariable(parts[0], parts[1]);
}
}
}
SQL_USER=username
SQL_PW=supersecret
SQL_SERVER=localhost
Before you can perform database operations, you need to establish a connection. This is achieved with a MySqlConnection object. We will add the variables from the .env file with string interpolation.
using MySql.Data.MySqlClient;
DotEnv.Load();
var env = Environment.GetEnvironmentVariables();
string cs = @$"
server={env["SQL_SERVER"]};
userid={env["SQL_USER"]};
password={env["SQL_PW"]};
database={env["SQL_DB"]}
";
var con = new MySqlConnection(cs);
con.Open();
Console.WriteLine($"MySQL version : {con.ServerVersion}");
con.Close();
After the connection has been established you will typically perform one or more operations, this is enabled by the MySqlCommand object. There are three primary methods that you will usually use:
Although possible to build SQL queries using string interpolation, it is not advisable to put query values directly in the query string. Doing so leaves your database open to injection attacks. Instead of placing the value into the query directly, we will use placeholders, prefixed with ‘@’.
string sql = $"SELECT Name FROM Country WHERE Continent = {continent};
string sql = "SELECT Name FROM Country WHERE Continent = @continent;
When using parameterized strings the values are added using the Parameters field of the MySqlCommand object.
To insert a new record into the database we use the ExecuteNonQuery method.
Executes a SQL statement returning the affected row count. [*]
string sql = "INSERT INTO users(username, password, email) values (@username, @password, @email)";
using var cmd = new MySqlCommand(sql, con);
cmd.Parameters.AddWithValue("@username", username);
cmd.Parameters.AddWithValue("@password", password);
cmd.Parameters.AddWithValue("@email", email);
cmd.ExecuteNonQuery();
Sends the CommandText value to MySqlConnection and builds a MySqlDataReader object.
The Reader object contains the rows that result from the SQL command execution. The Read method advances the reader to the next row, returning true if a row exists. The using keyword ensures that classes that implement the IDisposable interface call their dispose method.
using (MySqlDataReader reader = cmd.ExecuteReader()){
while (reader.Read()) {
...
}
}
You retrieve values from the current row with the class indexer which can retrieve values by column index or name. There are also several methods to retrieve a value by type, for example, GetInt32. The following statements perform the same action.
var name = reader[0];
var name = reader["name"];
var name = reader.GetString(0);
var name = reader.GetString("name");
Official MySQL/Net Tutorial
https://dev.mysql.com/doc/connector-net/en/connector-net-tutorials-sql-command.html
Nuget MySQL Package
https://www.nuget.org/packages/MySql.Data/
MySQL/Net API
Forward an HTTP request from one server to another while attaching data to the request. In the example the user will select a file from the Portal-Server and attach the file data on a request to the API-Server. We will also attach the user identification that was returned from the Authentication-Server.
When you make a POST request, you have to encode the data that forms the body of the request in some way [1]. This encoding type is communicated in the header field enctype, which has one of three values.
We will only concern ourselves with the first two encoding types.
Submit the POST with the parameters encoded in the body as a string of key-value pairs. Each pair is separated by an ‘&’ and the pair is seperated by ‘=’.
key1=value1&key2=value2
The key value pairs are inserted into the body and can be extracted by the server with the bodyParser package. The key-value pairs can also be directly attached to the URL request, in which case they are retrieved from the req.query field. See implementation.
Generally used for submitting file, or binary data. We will highlight the differences by manipulating the first example. We will keep everything else the same except for the encoding type. To retrieve the data on the server we will use the multer package.
The only difference in the multipart request as opposed to the urlencoded request is the in the body.
------WebKitFormBoundaryB2AGWn1yYcOCi0T9
Content-Disposition: form-data; name="jobid"
0
------WebKitFormBoundaryB2AGWn1yYcOCi0T9
Content-Disposition: form-data; name="jobname"
sdf
------WebKitFormBoundaryB2AGWn1yYcOCi0T9--
Instead of the ampersand delimited string of parameters, the parameters are now delimited by the unique boundary string “——WebKitFormBoundaryB2AGWn1yYcOCi0T9“. The boundary string is chosen in such a way as it does not appear anywhere else in the body data. Normally the programmer does not need to worry themselves about it, it is generated automatically. The boundary string is send in the HTTP content-type request header, appended to the type by a semicolon. See implementation.
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryB2AGWn1yYcOCi0T9
The simplest way to add a file is to include the file-input HTML tag on the form.
<input id="upload-dialog" type="file" name="fileupload">
The raw file is added to the body preceded by some file information. This is similar to how data fields get appended to the body.
------WebKitFormBoundaryvI9zg1SrRVBgB6yG
Content-Disposition: form-data; name="fileupload"; filename="iama_text_file.txt"
Content-Type: text/plain
In here is some simple text.
Two lines to fancy it up a bit.
------WebKitFormBoundaryvI9zg1SrRVBgB6yG--
To retrieve the file on the server we will again use the multer package, except this time we’ll use the ‘single‘ method. This will place the file in the ‘uploads‘ subdirectory. Note, the parameter we pass matches the name we gave to the upload-dialog above.
multer({ dest: 'uploads/' }).single('fileupload')
If you now examine the uploads/ subdirectory, you’ll find a file with a hash file for a name. This is because do not want to save raw filenames to your file-system. The uploaded file information is stored in the req.file field (see below). You can file the saved filename as well as the original filename here. You can sanitize the filename and move or copy the file as you see fit.
fieldname: 'fileupload',
originalname: 'iama_text_file.txt',
encoding: '7bit',
mimetype: 'text/plain',
destination: 'uploads/',
filename: '5f66c866e68790a2c494275829acd0b8',
path: 'uploads/5f66c866e68790a2c494275829acd0b8',
size: 61
Now that we understand the formatting of a file-upload, we want to manipulate a HTTP request to dynamically attach file data. The situation is that a file was previously uploaded and stored on the Portal-Server. Now we want to send that file to an API-Server with whatever other information is required. Doing this allows us to test the API-Server without special consideration, simply by using a web form (as we’ve been doing). Then when we deploy the API-Server we firewall it so that only the Portal-Server sees it. But, we proxy the call from the Portal-Server to the API-Server.
To facilitate proxies we are going to use the http-proxy-middleware package. To view the full code visit the github repository for this project.
The basic proxy requires a options object. The one we’ll be using is presented below. There are many more options available in the documentation.
{
target: "http://localhost:8000",
pathRewrite: { 'forward': 'raw' },
onProxyReq: appendFile
}
We will only be attaching the proxy to a single url. We call ‘multer’ first to extract existing parameters. Then we call the proxy, passing in the options object. This is all that is required for a simple proxy. What remains is attaching the file and data information to the request.
router.post("/forward",
multer({}).none(),
createProxyMiddleware(proxyOptions),
);
The onProxyReq function accepts three parameters: proxyReq, req, and res. We will only be concerned with the first two.
function appendFile(proxyReq, req, res) {
const args = {
...req.body,
userid: "user@id"
}
const filepath = Path.join(".", "uploads", req.body.filename);
const formData = new FormData();
Object.keys(args).map(key => formData.append(key, args[key]));
formData.append('fileupload', FS.readFileSync(filepath), 'file.txt');
proxyReq.setHeader('content-type', `multipart/form-data; boundary=${formData.getBoundary()}`);
proxyReq.setHeader('content-length', formData.getBuffer().length);
proxyReq.write(formData.getBuffer().toString("utf-8"));
proxyReq.end();
}
(8) The FormData class is a library to create readable "multipart/form-data"
streams. Can be used to submit forms and file uploads to other web applications.
(10) We add all values to the FormData object. We preserve old values (3) and append new values (4). This is optional and can be omitted if you want to remove the previous parameters.
(11) The file data is added to the FormData object using the readFileSync function which returns a Buffer object.
(13) We manually set the ready on the proxy request particularly to specify the boundary string.
(14) The FormData object will also provide us with the buffer length.
(16) The buffered FormData object is written out to proxyReq.
[1] https://www.w3.org/html/wg/spec/association-of-controls-and-forms.html#attr-fs-enctype
[2] https://nodejs.org/api/http.html#http_class_http_clientrequest
https://dev.to/sidthesloth92/understanding-html-form-encoding-url-encoded-and-multipart-forms-3lpa
https://stackoverflow.com/questions/37630419/how-to-handle-formdata-from-express-4
https://stackoverflow.com/questions/72809300/how-to-add-user-data-in-http-proxy-middleware
<form action="/multipart-data?id=x" method="post" enctype="multipart/form-data">
<b>multipart/form-data</b><br>
<span>jobid</span>
<input type="number" name="jobid" value="0"><br>
<span>jobname</span>
<input type="text" name="jobname" value=""><br>
<input type="submit"><br>
</form>
import express from "express";
import bodyParser from "body-parser";
const router = express.Router();
router.post("/multipart-data",
log,
multer({ dest: 'uploads/' }).none(),
printBody
);
function log(req, res, next) {
console.log(req.method + ` ` + req.originalUrl);
next();
}
function printBody(req, res, next){
console.log(req.body);
console.log(req.query);
next();
}
export default router;
POST /justdata
{ jobid: '0', jobname: 'asdf' }
{ id: 'x' }
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-CA,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 234
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryB2AGWn1yYcOCi0T9
------WebKitFormBoundaryB2AGWn1yYcOCi0T9
Content-Disposition: form-data; name="jobid"
0
------WebKitFormBoundaryB2AGWn1yYcOCi0T9
Content-Disposition: form-data; name="jobname"
sdf
------WebKitFormBoundaryB2AGWn1yYcOCi0T9--
<form action="/justdata?id=x" method="post" enctype="application/x-www-form-urlencoded">
<b>application/x-www-form-urlencoded</b><br>
<span>jobid</span>
<input type="number" name="jobid" value="0"><br>
<span>jobname</span>
<input type="text" name="jobname" value=""><br>
<input type="submit"><br>
</form>
import express from "express";
import bodyParser from "body-parser";
const router = express.Router();
router.post("/justdata",
log,
bodyParser.urlencoded({ extended: true }),
printBody
);
function log(req, res, next) {
console.log(req.method + ` ` + req.originalUrl);
next();
}
function printBody(req, res, next){
console.log(req.body);
console.log(req.query);
next();
}
export default router;
POST /justdata
{ jobid: '0', jobname: 'asdf' }
{ id: 'x' }
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-CA,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 20
Content-Type: application/x-www-form-urlencoded
jobid=0&jobname=asdf
sudo apt update
sudo apt upgrade
sudo apt install mysql-server
sudo service mysql start
sudo service mysql status
sudo mysql
sql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password by 'password';
sudo mysql_secure_installation
mysql -u root -p
sql> CREATE USER 'MyUsername'@'localhost' IDENTIFIED BY 'password';
sql> CREATE DATABASE datebase_name;
sql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, DROP, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON database_name.* TO 'MyUsername'@'localhost';
sql> quit
mysql -u MyUsername -p
sql> show databases;
sql> use database_name;
GRANT ALL ON demo.* TO 'demo'@'localhost';
mysql --login-path=demo
A Socket is one end of a two way communication link. The major protocols for socket communication are TCP and UDP. The primary difference is that TCP guarantees data delivery and the order of data packets. UDP does not make such guarantees, but as a consequence it is faster. Developers typically default to TCP.
The C# socket library is found inthe System.Net package.
using System.Net;
using System.Net.Sockets;
An IPEndpoint is the pairing of an IPAddress and a port number. You can use DNS lookup to obtain an IP address.
IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("google.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
IPEndPoint ipEndPoint = new(ipAddress, 7000);
When creating the server endpoint, you can specify ‘any’ for the ip address.
IPEndPoint ipEndPoint = new(IPAddress.Any, 7000);
Shutdown disables sends and receives on a Socket.
Close will terminate the Socket connection and releases all associated resources.
socket.Shutdown(SocketShutdown.Both);
socket.Close();
A server must first listen for and accept connections. Then, when a connection is made, listen for data on a seperate Socket.
Bind associates a socket with an endpoint.
Listen causes a connection-oriented (server) Socket to listen for incoming connection attempts.
Socket socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(ipEndPoint);
socket.Listen(port);
Accept blocks until an incoming connection attempt is queued then it extracts the first pending request from a queue. It then creates and returns a new Socket. You can call the RemoteEndPoint method of the returned Socket to identify the remote host’s network address and port number.
See AcceptAsync for the asynchronous accept call.
Socket handler = this.listener.Accept();
You create the client side socket in the same manner as the server. The difference being, instead of Bind and Listen, the client uses Connect.
IPAddress ipAdd = IPAddress.Parse("127.0.0.1");
IPEndPoint ipEndPt = new IPEndPoint(ipAdd, port);
Socket socket = new Socket(ipAdd.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ipEndPt);
Reading and writing date on a socket is the same for both the server and client side socket. Once the actual connection is made, the difference between the two is arbitrary. Both the read and operations require a byte array to act as a buffer. So your data will need to be converted to and from an array of bytes.
The Send method is used to write to a socket. While there are a number of different method flavours, the most common is to send the entire contents of a byte array.
byte[] msg = Encoding.ASCII.GetBytes(aString);
this.socket.Send(msg);
If you are using a connection-oriented protocol, Send will block until the requested number of bytes are sent, unless a time-out was set by using Socket.SendTimeout. If the time-out value was exceeded, the Send call will throw a SocketException.
If the receiving socket (server) has not been started a SocketException will be thrown. If the server has been started but Accept has not been called reading and writing will hang. This can be remedied by setting a timeout (in ms).
socket.ReceiveTimeout = 1000;
scoket.SentTimeout = 1000;
If the remote host shuts down the Socket connection with the Shutdown method, and all available data has been received, the Receive method will complete immediately and return zero bytes. This allows you to detect a clean shutdown.
There are many flavours of the Receive method, but we will only concern ourselves with two of them. The first reads all bytes from a socket. It returns the number of bytes read. You will need to provide your own EOF indicator or wait until 0 bytes are read which means the socket has finished writing and closed. This is useful if you are only connecting the sockets for a single read-write operation.
byte[] bytes = new byte[BUFFER_SIZE];
socket.Receive(bytes);
The second, and the one we will be using, is to read a specific number of bytes from the socket.
int count = socket.Receive(bytes, nextReadSize, SocketFlags.None);
We will use this to first read the size of the data, then read the body of the data.
// read size
byte[] bytes = new byte[INT_BUFFER_SIZE];
socket.Receive(bytes, INT_BUFFER_SIZE, SocketFlags.None);
int size = BitConverter.ToInt32(bytes, 0);
// read message
byte[] bytes = new byte[size];
socket.Receive(bytes, size, SocketFlags.None)
string data = Encoding.ASCII.GetString(bytes, 0, count);
The json encoding and decoding will be handled by the Newtonsoft Json.Net library.
dotnet add package Newtonsoft.Json --version 13.0.1
The include statements for this library are.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
There are 3 source files in the project:
Most of the details for this class are provided in the above background section. The main detail here is the way new connections are managed. I have put the loop inside a IEnumerable method. This should be looped to hand off new connection objects. The following example does this in a new thread.
Thread thread = new Thread(new ThreadStart(()=>{
foreach(Connection connection in server.Connections()){
connection.WriteString("ack");
connection.Close();
}
}));
The connection object reads and writes JSON objects or Strings in two parts. First it writes four bytes representing the integer size of the data. Next it writes the data it’s self.
Client client = new Client().Connect("127.0.0.1", 7000);
client.socket.ReceiveTimeout = 3000;
Console.WriteLine("> " + client.ReadString());
server.Stop();
This instruction post is an abbreviation of Microsoft tutorial found HERE.
An SLN is a Visual Studio solution file that keeps information about the organization of projects in a file.
dotnet new sln
The -o (–output) flag specifies the directory to place the project. It will create the directory.
dotnet new classlib -o StringLibrary
dotnet sln add StringLibrary/StringLibrary.csproj
Add the following code to the StringLibrary/Class1.cs file
namespace UtilityLibraries;
public static class StringLibrary
{
public static bool StartsWithUpper(this string? str)
{
if (string.IsNullOrWhiteSpace(str))
return false;
char ch = str[0];
return char.IsUpper(ch);
}
}
dotnet build
dotnet new console -o ShowCase
dotnet sln add ShowCase/ShowCase.csproj
using UtilityLibraries;
class Program {
static void Main(string[] args) {
foreach(string s in args){
var uc = s.StartsWithUpper();
Console.WriteLine($"String {s} starts with uppser case {uc}");
}
}
}
dotnet add ShowCase/ShowCase.csproj reference StringLibrary/StringLibrary.csproj
dotnet run --project ShowCase/ShowCase.csproj AB ab
dotnet new mstest -o StringLibraryTest
dotnet add StringLibraryTest/StringLibraryTest.csproj reference StringLibrary/StringLibrary.csproj
dotnet test StringLibraryTest/StringLibraryTest.csproj
Add the following to your project’s .csproj file. Change the StringLibrary project name as necessary.
<ItemGroup>
<Reference Include="StringLibrary">
<HintPath>./lib/StringLibrary.dll</HintPath>
</Reference>
</ItemGroup>