thesis/tex/chapters/4-projekti.tex
2020-05-09 12:07:59 +03:00

153 lines
18 KiB
TeX

\chapter{Projekti}
Tein insinöörityön yhteydessä full-stack projektin, jossa sekä palvelin- että asiakaspuolen ohjelmointi tehtiin Rustilla. Tarkoituksena ei ollut saada aikaiseksi mitään todella monimutkaista ohjelmaa, vaan puhtaasti arvioida Rustin soveltuvuutta web-ohjelmointiin.
\section{Kehitysympäristön asennus}
Rust-projektin aloittamiseksi kehittäjä tarvitsee koneelleen Rustin paketinhallintatyökalun, Cargon (katso luku \ref{sect:paketinhallinta}). Olen itse Linux-käyttäjä, joten sain asennettua Cargon Linux-jakeluni ohjelmavarastosta. Windows-käyttäjille suosittelen käyttämään Rustup-asennusohjelmaa. Lisää tietoa Rustin asentamisesta saa Rustin kotisivuilta \cite{rust:install}.
Projektin saa initialisoitua komennolla \mintinline{shell}{cargo init projektinnimi}. Tämä komento luo hakemiston ''projektinnimi'', minkä sisällä on Cargon konfiguraatiotiedosto Cargo.toml, jossa voi määrittää projektin tiedot ja riippuvuudet.
Lähdekoodi sijaitsee hakemistossa ''src''. Cargo kirjoittaa hakemistoon valmiiksi ''main.rs'' -tiedoston, jossa on ''Hello world!'' esimerkkikoodi. ''main.rs'' on aina Rust-ohjelman ensimmäiseksi suoritettava tiedosto, eli niin kutsuttu entrypoint. ''main.rs''-tiedoston sisällä pitää olla ''main()''-funktio, josta ohjelman suoritus alkaa. Projektin voi suorittaa komennolla \mintinline{shell}{cargo run}.
Suosittelen asentamaan myös muutaman Cargon liitännäisen helpottamaan kehitystä. cargo-watchin avulla voi ajaa komennon aina, kun lähdekoodi muuttuu. Esimerkiksi komennolla \mintinline{shell}{cargo watch -x run} voi kääntää ja käynnistää projektin aina uudelleen, kun lähdekoodi muuttuu. cargo-add -liitännäisellä voi helposti lisätä uusia riippuvuuksia projektiinsa. Esimerkiksi Actix webin saa lisättyä komennolla \mintinline{shell}{cargo add actix-web}.
Näillä ohjeilla pääsee alkuun palvelinpuolen kehityksessä. Asiakaspuolen kehityksen käynnistäminen vaati vielä muutaman lisävaiheen, siitä lisää luvussa \ref{sect:asiakaspuoli:asennus}.
\section{Palvelinpuoli}
\subsection{Kehys}
Palvelinpuolen kehykseksi valikoitui Actix web. Se on käytännössä vastine JavaScript-maailman Express.js:lle, eli se hoitaa HTTP-palvelimen työtä ja reitittää GET- ja POST-pyynnöt ohjelman oikeille funktioille.
Actix web on puolestaan rakennettu hyödyntämällä Actix-sovelluskehystä, joka on rakennettu löyhästi actor-mallin pohjalta. Actor-malli \cite{wiki:actor} on Carl Hewittin vuonna 1973 luoma matemaattinen ja tietotekninen malli rinnakkaisajosta. Tämän ansiosta Actix web on hyvin suorituskykyinen ja helposti skaalautuva ratkaisu rajapintoja rakennettaessa.
Actix web on myös hyvin integroitu Rustin vahvaan tyypitykseen. Erityisen vaikuttavaa oli se, että esimerkiksi tietyn rajapinnan päätepisteen POST-pyyntöön pystyy määrittämään parametriksi tietyn rakenteen (eng. structure) [koodiesimerkki \ref{code:rust:registeruser}].
\bigskip
\begin{code}
\inputminted{Rust}{code/registeruser.rs}
\captionof{listing}{Rakenne POST-pyynnön parametrinä}
\label{code:rust:registeruser}
\end{code}
Tällöin varmistutaan automaattisesti siitä, että kun asiakkaan POST-pyyntö saapuu tähän funktioon, kaikki data on oikeanlaista tyyppiä ja kaikki tarvittava data on pyynnössä mukana. Verrattuna JavaScriptiin, jossa ei ole tyyppejä, kaikki vastaanotettava data pitäisi tarkastaa käsin.
\begin{code}
\inputminted{JSON}{code/registeruser.json}
\captionof{listing}{Rakenteeseen sovitettava JavaScript-objekti}
\label{code:json:registeruser}
\end{code}
Kun koodiesimerkin \ref{code:rust:registeruser} funktioon ''register'' saapuu koodiesimerkin \ref{code:json:registeruser} mukainen JSON-objekti, Actix huomaa väärän tyypin ja vastaa statuskoodilla ''400 Bad Request'', koska objektin parametri \mintinline{JSON}{"admin"} ei ole tyyppiä \mintinline{Rust}{bool}.
Palvelinpuolen initialisointi ja käynnistäminen löytyvät liitteestä \ref{appx:actix}.
\subsection{Todentaminen}
Actix webin modulaarisuus mahdollisti myös helposti tarvittavien väliohjelmistojen sisällyttämisen ohjelman toimintaan. Actix identity -paketista löytyi tarvittavat palikat, joilla sain lisättyä itse kirjoittamani käyttäjän todentamistoiminnallisuuden suojaamaan haluttuja reittejä.
Todentamiseen päätin käyttää JSON Web Tokeneita. Ne ovat standardoitu (RFC 7519\cite{jwt:home}) tunnistautumistapa verkossa. Tokenit ovat merkkijonoja, jotka sisältävät JavaScript-objektin tekstimuodossa (JSON). Yhden tällaisen tokenin sisällön voi nähdä koodiesimerkissä \ref{code:json:jwt}.
\bigskip
\begin{code}
\inputminted{Rust}{code/jwt.json}
\captionof{listing}{Yhden JSON Web Tokenin sisältö tästä projektista}
\label{code:json:jwt}
\end{code}
Tämä tokenin sisältö on kaikkien sen hallussapitäjien nähtävissä. Tokenin turvallisuus tulee siitä, että token on allekirjoitettu palvelinpuolella vain palvelimen tiedossa olevalla salasanalla niin, että jos tokenin sisältö muuttuu yhtään, palvelin näkee että se ei ole enää validi.
Otetaan esimerkkinä tämän sovelluksen käyttäjä Pasi. Pasi ei ole ylläpitäjä, mutta hän koittaa tehdä itsestään ylläpitäjän muuttamalla omasta tokenistaan parametrin \mintinline{JavaScript}{"admin": false} arvoksi \mintinline{JavaScript}{true}. Kun Pasi lähettää tämän muokatun tokenin palvelimelle, se viedään JWT-validointifunktioon. Palvelin huomaa, että token ei ole enää validi, koska sen sisältöä ei ole allekirjoitettu palvelimen salasanalla.
Tokenin sisällön voi päättää kokonaan itse, vaikkakin joitakin standardeja kenttiä on määritetty. Esimerkiksi iss (issuer), sub (subject) ja exp (expiration time). Päätin sisällyttää myös tiedon siitä, että onko käyttäjä ylläpitäjä, koska tätä tietoa voi sitten käyttää asiakaspuolella esimerkiksi käyttöliittymän muokkaamiseen käyttäjän roolin perusteella. Usein myös käyttäjän nimi sisällytetään tokeniin. Tokenin sisältöä suunnitellessa kannattaa pitää mielessä, että sen sisältö on nähtävissä kaikille, joten se ei ole oikea paikka tallettaa salaista tietoa, kuten vaikka käyttäjän salasana.
Päätin tallettaa edellä mainitun JSON Web Tokenin evästeeseen (eng. cookie), joka on standardi tapa tallettaa juuri tällaisia todentamiseen käytettäviä tietoja selaimiin. Evästeiden käyttämisen etu on se, että selain huolehtii sen tallettamisesta automaattisesti ilman lisätoimia kehittäjältä. Lisäksi selain sisällyttää sen seuraaviin kutsuihin automaattisesti.
Projektin koko JWT-toteutus on liitteessä \ref{appx:jwt}.
\subsection{CORS}
Lisäsin palvelimelle myös Cross-Origin Resource Sharing (CORS) \cite{wiki:cors} -toiminnallisuuden. Oletuksena selaimen lataama ohjelma saa ladata resursseja vain samasta osoitteesta, kuin mistä itse ohjelma on ladattu. Tämä on turvallisuuskäytäntö, joka kulkee nimellä Same-origin policy (SOP) \cite{wiki:sop}. Näihin resursseihin sisältyvät muunmuassa CSS-tyylimääritykset, kuvat ja JavaScript-ohjelmat.
Lähes kaikki nykyiset web-ohjelmistot kuitenkin vaativat näiden ulkoisien resurssien käyttöä. CORS on tekniikka, jossa palvelin kertoo selaimelle sallitut osoitteet, mistä resursseja saa noutaa. CORS:n lisääminen tähän projektiin hoitui Actixin liitännäisellä actix\char`_cors.
CORS:n asetusten määritys löytyy liitteestä \ref{appx:actix}.
\subsection{Tietokanta}
\label{project:database}
Tietokannaksi valikoitui itselleni tuttu MySQL. Relaatiotietokannan sai helposti yhdistettyä Rust-koodiini Diesel ORM:llä. Diesel on tähän mennessä käyttämistäni ORM-kirjastoista selkeästi mukavin käyttää.
Käytännöllisimmät ominaisuudet kehittäjän näkökulmasta olivat Dieselin mukana tuleva komentorivikäyttöliittymä ja migraatiot. Jokaiselle taululle luodaan uusi migraatio, esimerkiksi \mintinline{shell}{diesel migration generate users}, jonka jälkeen Dieselin luomaan hakemistoon kirjoitetaan up.sql- ja down.sql-tiedostot, eli ohjeet siitä, miten tämä taulu luodaan ja poistetaan. Taulu viedään tietokantaan komennolla \mintinline{shell}{diesel migration run} ja taulun voi poistaa ja luoda uudelleen komennolla \mintinline{shell}{diesel migration redo}. Tämä mahdollistaa myös samalla sen, että versiohallintaan voi tallentaa useita versioita samasta taulusta ja palata helposti takaisin vanhempaan versioon, jos uudemman kanssa ilmenee ongelmia.
Edellä mainitut työkalut helpottivat tietokannan kehitystä huomattavasti. Usein varsinkin projektin alkuvaiheilla tietokanta muuttuu jatkuvasti ja usein tulee tarve poistaa ja luoda tietokanta uudelleen. Monet kehittäjät pitävät juurikin tällaista Dieselin up.sql kaltaista tiedostoa versiohallinnassa ja tarpeen mukaan poistavat tietokannan käsin ja liittävät komennon tiedostosta tietokannan komentorivikäyttöliittymään. Dieseliä käytettäessä tämä tulee tehtyä automaattisesti ja se tuntuu todella luontevalta.
Päätin myös käyttää Dieselin kanssa yhteyksien yhdistämistä (eng. connection pooling). Tämä tarkoittaa sitä, että palvelin luo käynnistyessään prosessin, joka ottaa yhteyden tietokantaan, ja jakaa tätä yhteyttä sitä tarvitseville ohjelman osille. Tämä on tehokkaampaa kuin vaihtoehto, jossa otetaan uusi yhteys jokaista tietokantakyselyä varten.
\section{Asiakaspuoli}
\subsection{Asennus}
\label{sect:asiakaspuoli:asennus}
Projektin asiakaspuolen aloittaminen vaati aika paljon tutkimustyötä. Rust-koodi pitää kääntää WebAssemblyksi, johon on olemassa useita eri työkaluja. Lisäksi selaimien rajapintojen käyttämiseen tarvitsee jonkinlaisen kirjaston, joita Rust-maailmassa on tällä hetkellä kaksi: stdweb\cite{webassembly:stdweb} ja web\char`_sys\cite{webassembly:websys}. Näistä stdweb on vanhempi, mutta myös tuetumpi. web\char`_sys on uudempi tulokas ja näyttää siltä, että se tulee korvaamaan stdweb:n tulevaisuudessa.
Valitsemani sovelluskehys tukee molempia kirjastoja, mutta päädyin kuitenkin valitsemaan stdweb:n. Suurimpana syynä oli projektin aloittamisen helppous, joka tehdään Cargon liitännäisellä cargo-web. Toisena syynä oli, että WebAssembly-ohjelman paketointi web\char`_sys:n kanssa on tätä kirjoittaessani riippuvainen NodeJS:stä, kun taas stdweb ei vaadi NodeJS:n asennusta kehittäjän koneelle ollenkaan.
WebAssembly-koodin suorittamiseksi selain tarvitsee pienen pätkän JavaScriptiä, joka tekee tarvittavat toimet WebAssembly-ohjelman käynnistämiseksi. Tämän koodin generoi puolestani cargo-web. Lisäksi JavaScriptin suorittamiseen tarvitaan yksi HTML-tiedosto. Tähän tiedostoon voi myös sisällyttää metadataa, kuten esimerkiksi sivuston otsikon, joka näkyy selaimen välilehdessä. Tähän tiedostoon linkitetään myös kaikki muu staattinen data, kuten tyylimääritykset. Nämä tiedostot laitoin käytännön mukaisesti static-nimiseen hakemistoon asiakaspuolen projektin juureen. Tämän hakemiston linkitin symbolisella linkillä palvelinpuolen projektiin, jonka HTTP-serveri voi sitten lähettää HTML-dokumentin, JavaScript-tiedoston ja WebAssembly-binäärin käyttäjän selaimelle.
\subsection{Kehys}
Asiakaspuolen sovelluskehykseksi valitsin Yew:n\cite{yew:home}. Yew muistuttaa hyvin paljon JavaScript-maailmassa suosittua Reactia, eli sen on komponenttipohjainen. Tämä tarkoittaa sitä, että kaikkien ohjelman osien, joilla halutaan näyttää jotakin käyttöliittymän osaa, täytyy implementoida Yew:n Component-rajapintaa. Tässä rajapinnassa on funktiot create, change, update ja view. Näiden funktioiden avulla Yew pystyy orkestroimaan kullakin hetkellä näytettävien komponenttien tilaa.
\subsection{Ulkonäkö}
\label{sect:ulkonäkö}
Olen JavaScript-maailmassa tottunut siihen, että käyttöliittymäkehyksiin löytyy usein kirjasto, joka tarjoaa valmiit tyylimääritykset, mutta en löytänyt vastaavaista kirjastoa, joka toimisi Yew:n kanssa. Kaikki käyttöliittymän elementit olivat siis selaimen oletustyylisiä. En halunnut ryhtyä tässä projektissa kirjoittamaan CSS-määrityksiä alusta alkaen, joten päädyin käyttämään UIkittiä\cite{uikit}. UIkit on CSS-kirjasto, missä on paljon valmiita ja hyvännäköisiä tyylimäärityksiä. UIkitin dokumentaatio on laaja ja siellä on paljon esimerkkejä, joten alkuun pääsi todella nopeasti [kuva \ref{fig:login}].
\begin{figure}[h]
\centering
\includegraphics[width=\linewidth]{illustration/login.png}
\caption{Login-komponentin tyylittely, joka on toteutettu käyttämällä UIKit-kirjastoa}
\label{fig:login}
\end{figure}
Login-komponentti löytyy kokonaisuudessaan liitteestä \ref{appx:login}. Liitteestä näkee esimerkin Yew:n komponenttirajapinnan käytöstä ja palvelimeen yhdistämisestä. Lisäksi siellä on käytetty UIKit-tyylityksiä.
\subsection{Reititys}
Reititykseen käytin Yew:n liitännäistä, yew\char`_routeria. Reititys asiakaspuolen ohjelmassa tarkoittaa sitä, että selaimen osoitepalkissa olevan polun mukaan käyttäjä reititetään oikeaan ohjelman osaan. Tämä liittyy käsitykseen yhden sivun ohjelmista (katso luku \ref{sect:spa}), joissa selain suorittaa yhden ohjelman, jonka jälkeen perinteisiä sivujen latauksia ei enää tapahdu. Ohjelma muokkaa osoitepalkissa näkyvää osoitetta, jonka pohjalta sitten reititys oikeaan komponenttiin tapahtuu. Käyttöliittymää muokataan käyttämällä DOM:ia, joka mahdollistaa sivun sisällön muokkaamisen ilman sivun uudelleen lataamista.
Asiakaspuolen reititys vaatii palvelinpuolelta sen, että kaikki mahdolliset asiakaspuolen reitit palauttavat ohjelman. Toinen edellytys on se, että palvelin ei palauta uudelleenohjausta (HTTP 302), koska silloin myös osoitepalkissa oleva osoite muuttuu, eikä sitä silloin voida välittää asiakaspuolen ohjelmalle.
Yritin pitkään toteuttaa tällaista logiikkaa palvelinpuolelle siinä onnistumatta, mutta onnekseni yew\char`_routerin esimerkeistä löytyi esimerkkikoodia tämän saavuttamiseksi käyttämäni palvelinkehyksen kanssa. Päädyin laittamaan kaikki palvelimen omat reitit polun \mintinline{shell}{/api/} alle, ja asiakaspuolen ohjelman juureen (\mintinline{shell}{/}). Sitten määritin ns. ''catch-all'' reitin, joka palauttaa asiakaspuolen ohjelman ilman uudelleenohjausta. Tämä on Actixissa nimeltään \mintinline{shell}{default_service}.
Myös yew\char`_routerin kanssa oli omat haasteensa. Alkuun en saanut sitä toimimaan ollenkaan, reititin ohjasi kaikki osoitteet ensimmäisenä määritettyyn reittiin, mikä oli tässä tapauksessa ''/''. Ongelman syyksi paljastui yew\char`_routerin Switch-komponentin kokoava ''to'-makro (katso luku \ref{chap:macros}). Ratkaisuna oli reitin ''/'' makron siirtäminen viimeiseksi listassa. Tarkempaa tietoa tästä ongelmasta voi lukea Yew:n dokumentaatiosta \cite{yew:router-problem}.
Asiakaspuolen reitityksen toteutuksen voi nähdä liitteessä \ref{appx:frontend-main}.
\section{Ongelmat}
Projektin loppuvaiheilla ilmeni muutamia ongelmia, jotka vaativat vielä jatkokehitystä.
\subsection{Evästeet}
\label{sect:problems:cookies}
Palvelinpuolella evästeen kokoava kirjasto, Actixin liitännäinen actix\char`_identity, kirjoittaa oletuksena evästeeseen parametrin \mintinline{JavaScript}{"HttpOnly": true}, mikä tarkoittaa sitä, että selain ei anna sen sisällä suoritettaville ohjelmille pääsyä tähän evästeeseen. Tämä on todella hyvä turvallisuusominaisuus, joka estää haitallisia ohjelmia varastamasta käyttäjän kirjautumistietoja.
Suunnittelmana oli käyttää evästeen tietoja asiakaspuolella reititykseen. Esimerkiksi jos evästettä ei ole olemassa, käyttäjä tulee reitittää sisäänkirjautumiskomponenttiin, jossa eväste voidaan noutaa palvelimelta. Koska evästeessä on tämä HttpOnly-parametri, tähän ei ole mahdollisuutta. Tämän parametrin voisi tietysti laittaa pois päältä, mutta actix\char`_identity ei tarjoa tähän mitään mahdollisuutta.
Yhtenä ratkaisuna tähän ongelmaan olisi käyttää JSON Web Tokenin tallettamiseen evästeen sijasta selaimen LocalStoragea. Tämä vaatisi lisää työtä sekä asiakaspuolella että palvelinpuolella.
\subsection{Tietokantayhteys}
Palvelimen ja tietokannan välisessä yhteydessä oli koko projektin ajan ongelma, että jokainen kysely tietokantaan kesti noin viisi sekuntia. Luulen että tämä ongelma liittyy jotenkin käyttämääni yhteyksien yhdistämiseen (luku \ref{project:database}), koska palvelimen loki näyttää satunnaisesti virheviestin \mintinline{shell}{Failed to create DB pool: Error(Some("Too many connections")}. Asian selvittäminen jää jatkokehitykseen.
\section{Tulokset}
Tuloksena syntyi yksinkertainen web-ohjelma, jossa on toimiva sisäänkirjautumistoiminallisuus, jonka avulla palvelimelta asiakas voi noutaa suojattua dataa. Projektissa sekä palvelin- että asiakaspuoli on toteutettu Rustilla. Kuvassa \ref{fig:login-process} on määritelty kaikki kirjautumisprosessin vaiheet.
\begin{figure}[h]
\centering
\includegraphics[width=\linewidth]{illustration/login-process.pdf}
\caption{Kirjautumisprosessi projektissa}
\label{fig:login-process}
\end{figure}
Ohjelman juuressa renderöidään komponentti ''ProtectedComponent'', missä on yksi nappi, jolla voidaan noutaa dataa palvelimelta. Tämä data on suojattu niin, että pyynnön mukana pitää olla eväste, jonka sisällä on JWT, joka sitten validoidaan palvelinpuolella. Jos validointi onnistuu, suojattu data lähetetään asiakkaalle. Asiakaspäässä HTTP-statuskoodin perusteella näytetään joko noudettu data, tai "403 Unauthorized".
Reitissä ''/login'' renderöidään ''LoginComponent'', joka koostuu kahdesta tekstikentästä, käyttäjänimi ja salasana, sekä napista, jolla kirjautumistiedot voi lähettää palvelimelle. Onnistumisen kirjautumisen jälkeen palvelimelta lähetetty eväste tallennetaan selaimen muistiin.
Luvussa \ref{sect:problems:cookies} mainittujen ongelmien takia toteutus jäi hieman kesken. Eväste saadaan onnistuneesti palvelimelta selaimeen, mutta koska eväste ei ole ohjelman saatavilla, en voi toteuttaa tämän perusteella reititystä uudelle sivulle. Tein tämän takia sivun ylälaitaan napit, joilla käyttäjä voi siirtyä manuaalisesti sivulta toiselle. Ajatuksena oli, että onnistuneen kirjautumisen jälkeen käyttäjä siirrettäisiin toiselle sivulle, ja evästeen puuttueessa taas kirjautumissivulle.