Gitea functionality.

pull/11/head
pebles 3 years ago
parent 751cbf2616
commit 29b84e9d57
Signed by: pebles
GPG Key ID: 4501C52A9268B49C
  1. 7
      examples/lowry.conf
  2. 138
      gitea/gitea.go
  3. 29
      gitea/sshkeygen.go
  4. 47
      gitea/webhook.go
  5. 32
      main.go
  6. 101
      server/gitea.go
  7. 26
      server/server.go
  8. 1
      server/template.go
  9. 50
      tmpl/gitea.html
  10. 3
      tmpl/index.html

@ -2,3 +2,10 @@ ldapaddr=localhost:389
ldappass=foobar
domain=nodomain
httpaddr=:8080
giteaURL=
token=
cloneAddr=
webhookRepoURL=
webhookRepoSecret=
webhookURL=
webhookSecret=

@ -0,0 +1,138 @@
package gitea
import (
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/sdk/gitea"
"log"
"net/url" // to check URL init vars
"reflect" // to compare structs
)
// Gitea holds the gitea functionality config vars
type Gitea struct {
GiteaURL, Token, CloneAddr, WebhookRepoURL, WebhookRepoSecret, WebhookURL, WebhookSecret, Repo string
}
func isValidURL(toTest string) bool {
_, err := url.ParseRequestURI(toTest)
if err != nil {
return false
} else {
return true
}
}
// Init gitea config vars
func Init(giteaURL, token, cloneAddr, webhookRepoURL, webhookRepoSecret, webhookURL, webhookSecret string) *Gitea {
// check URL values
if !isValidURL(giteaURL) || !isValidURL(cloneAddr) || !isValidURL(webhookRepoURL) || !isValidURL(webhookURL) {
giteaURL = "" // to avoid process
log.Println("Some config URL fields are not valid.")
}
return &Gitea{
GiteaURL: giteaURL,
Token: token,
CloneAddr: cloneAddr,
WebhookRepoURL: webhookRepoURL,
WebhookRepoSecret: webhookRepoSecret,
WebhookURL: webhookURL,
WebhookSecret: webhookSecret,
}
}
func GiteaNewClient(giteaURL, token string) *gitea.Client {
client := gitea.NewClient(giteaURL, token)
return client
}
func GiteaUserID(user string, client *gitea.Client) (int, error) {
// check users exists and return user id
userexist, err := client.SearchUsers(user, 1)
if err != nil {
return -1, err
}
emptyuser := []*gitea.User{}
if reflect.DeepEqual(userexist, emptyuser) == true {
log.Printf("User doesn't exist")
}
uid := int(userexist[0].ID)
return uid, nil
}
func RepoExists(user string, repo string, client *gitea.Client) bool {
// check repo exists.
repoexists, err := client.GetRepo(user, repo)
if err != nil && err.Error() != "404 Not Found" {
return false
}
// (API responses with empty repo if doesn't exist.)
emptyrepo := &gitea.Repository{}
if reflect.DeepEqual(repoexists, emptyrepo) == true {
return false
} else {
return true
}
}
func RepoDelete(user string, repo string, client *gitea.Client) error {
// delete repo
err := client.DeleteRepo(user, repo)
if err != nil {
log.Printf("Error on delete repo.")
return err
}
return nil
}
func RepoMigrate(cloneAddr string, uid int, repo string, client *gitea.Client) error {
// migrate template repo to new user repo
migrateRepoOption := structs.MigrateRepoOption{CloneAddr: cloneAddr,
Description: "Basic hugo site repo",
UID: uid,
RepoName: repo,
Private: true}
_, err := client.MigrateRepo(migrateRepoOption)
if err != nil {
return err
}
return nil
}
func RepoDeployKey(keypub, user, repo string, client *gitea.Client) error {
// put a deploy key on user repo
deployKeyOption := structs.CreateKeyOption{
Key: keypub,
ReadOnly: true,
Title: "Web deploy key"}
_, err := client.CreateDeployKey(user, repo, deployKeyOption)
if err != nil {
return err
}
return nil
}
func RepoCreateWebhook(webhookRepoURL, webhookRepoSecret, user, repo string, client *gitea.Client) error {
// create a webhook on user repo
webHookOption := structs.CreateHookOption{
Active: true,
Config: map[string]string{"content_type": "json",
"url": webhookRepoURL,
"secret": webhookRepoSecret},
Events: []string{"push"},
Type: "gitea"}
_, err := client.CreateRepoHook(user, repo, webHookOption)
// TODO: <nil> no vale pa na
if err != nil && err.Error() != "<nil>" {
log.Printf("API Error on migrate.")
return err
}
return nil
}

@ -0,0 +1,29 @@
package gitea
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"golang.org/x/crypto/ssh"
)
func GenerateRSAKeyPair(bits int) (userkeypriv, userkeypub string, err error) {
// generate private key pair, private key with pem format and public key with authorized_keys format
var privateKey *rsa.PrivateKey
privateKey, _ = rsa.GenerateKey(rand.Reader, bits)
privateKeyDer := x509.MarshalPKCS1PrivateKey(privateKey)
privateKeyBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privateKeyDer,
}
privateKeyPem := pem.EncodeToMemory(&privateKeyBlock)
publicKey := privateKey.PublicKey
pub, _ := ssh.NewPublicKey(&publicKey)
userkeypriv = string(privateKeyPem)
userkeypub = string(ssh.MarshalAuthorizedKey(pub))
return
}

@ -0,0 +1,47 @@
package gitea
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
)
func WebhookDeploy(user, repo, keypriv, keypub, webhookURL, webhookSecret string) error {
type WebhookData struct {
USER string
REPO string
PUB string
KEY string
SECRET string
}
keypriv = strings.Replace(keypriv, "\n", "|", -1) // key to oneline string, no breaks, '|' (pipe) is newline.
whData := WebhookData{user, repo, keypub, keypriv, webhookSecret}
jsonData, err := json.Marshal(whData)
if err != nil && err.Error() != "<nil>" {
log.Printf("Error marshaling data for webhook.")
return err
}
req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
// webhook listener must responses 400 if trigger rule (webhooksecret) is not satisfied
if resp.StatusCode == 400 {
return fmt.Errorf("Auth fail: %d", resp.StatusCode)
} else {
return fmt.Errorf("http code error: %d", resp.StatusCode)
}
}
defer resp.Body.Close()
return nil
}

@ -9,6 +9,7 @@ import (
"time"
"0xacab.org/sindominio/lowry/db"
"0xacab.org/sindominio/lowry/gitea"
"0xacab.org/sindominio/lowry/ldap"
"0xacab.org/sindominio/lowry/mail"
"0xacab.org/sindominio/lowry/server"
@ -23,16 +24,23 @@ var (
func main() {
var (
ldapaddr = flag.String("ldapaddr", "localhost:389", "LDAP server address and port")
domain = flag.String("domain", "", "LDAP domain components")
ldappass = flag.String("ldappass", "", "Password of the LDAP `admin' user")
homepath = flag.String("homepath", "/home/", "Path to the user homes")
smtpaddr = flag.String("smtpaddr", "localhost:25", "The address of the smtp server to send email")
email = flag.String("email", "", "The email address to send notifications from")
emailpass = flag.String("emailpass", "", "The password of the email address")
httpaddr = flag.String("httpaddr", ":8080", "Web server address and port")
dbpath = flag.String("dbpath", "bolt.db", "The path to store the lowry status database")
ro = flag.Bool("ro", false, "Read-Only mode")
ldapaddr = flag.String("ldapaddr", "localhost:389", "LDAP server address and port")
domain = flag.String("domain", "", "LDAP domain components")
ldappass = flag.String("ldappass", "", "Password of the LDAP `admin' user")
homepath = flag.String("homepath", "/home/", "Path to the user homes")
smtpaddr = flag.String("smtpaddr", "localhost:25", "The address of the smtp server to send email")
email = flag.String("email", "", "The email address to send notifications from")
emailpass = flag.String("emailpass", "", "The password of the email address")
httpaddr = flag.String("httpaddr", ":8080", "Web server address and port")
dbpath = flag.String("dbpath", "bolt.db", "The path to store the lowry status database")
ro = flag.Bool("ro", false, "Read-Only mode")
giteaURL = flag.String("giteaURL", "", "Gitea server address")
token = flag.String("token", "", "Gitea admin token")
cloneAddr = flag.String("cloneAddr", "", "Template repo address to copy")
webhookRepoSecret = flag.String("webhookRepoSecret", "", "Webhook secret of the created repo")
webhookRepoURL = flag.String("webhookRepoURL", "", "Webhook url of the created repo")
webhookURL = flag.String("webhookURL", "", "Webhook URL to send user keys")
webhookSecret = flag.String("webhookSecret", "", "Webhook Secret to send user keys")
)
flag.String(flag.DefaultConfigFlagname, "/etc/lowry.conf", "Path to configuration file")
flag.Parse()
@ -49,6 +57,8 @@ func main() {
if err != nil {
log.Fatal(err)
}
g := gitea.Init(*giteaURL, *token, *cloneAddr, *webhookRepoURL, *webhookRepoSecret, *webhookURL, *webhookSecret)
go lockUsers(l)
ldb, err := db.Init(*dbpath)
@ -58,7 +68,7 @@ func main() {
defer ldb.Close()
go cleanInvites(ldb)
log.Fatal(server.Serve(*httpaddr, &l, m, ldb))
log.Fatal(server.Serve(*httpaddr, &l, m, ldb, g))
}
func lockUsers(l ldap.Ldap) {

@ -0,0 +1,101 @@
package server
import (
"0xacab.org/sindominio/lowry/gitea"
"log"
"net/http"
)
func (s *server) giteaHandler(w http.ResponseWriter, r *http.Request) {
response := s.newResponse("gitea", w, r)
if r.Method != "POST" {
response.execute("")
return
}
var (
giteaURL = s.gitea.GiteaURL
token = s.gitea.Token
cloneAddr = s.gitea.CloneAddr
webhookRepoURL = s.gitea.WebhookRepoURL
webhookRepoSecret = s.gitea.WebhookRepoSecret
webhookURL = s.gitea.WebhookURL
webhookSecret = s.gitea.WebhookSecret
usergitea = response.User
repo = r.FormValue("reponame")
)
// check config vars. giteaURL is empty if any URL fields are empty, init (gitea package) cares about it.
if giteaURL == "" || token == "" || webhookRepoSecret == "" || webhookSecret == "" {
response.execute("ErrorGiteaInit")
log.Printf("Gitea config isn't right, some fields are empty.")
return
}
// create client connection to gitea
client := gitea.GiteaNewClient(giteaURL, token)
uid, err := gitea.GiteaUserID(usergitea, client)
// clean existing repo
if gitea.RepoExists(usergitea, repo, client) {
if err := gitea.RepoDelete(usergitea, repo, client); err != nil {
log.Printf("Error deleting gitea repo: %v ", err)
response.execute("Error")
return
}
}
// create repo from template repo
if err = gitea.RepoMigrate(cloneAddr, uid, repo, client); err != nil {
log.Printf("Error migrating template repo: %v", err)
response.execute("Error")
return
}
// generate keys
keypriv, keypub, err := gitea.GenerateRSAKeyPair(4096)
if err != nil {
log.Printf("Error generating key pair: ", err)
if err := gitea.RepoDelete(usergitea, repo, client); err != nil {
log.Printf("Error deleting gitea repo: %v", err)
}
response.execute("Error")
return
}
// create deploy key on repo
if err = gitea.RepoDeployKey(keypub, usergitea, repo, client); err != nil {
log.Printf("Error deploying key on gitea repo: %v", err)
if err := gitea.RepoDelete(usergitea, repo, client); err != nil {
log.Printf("Error deleting gitea repo: %v", err)
}
response.execute("Error")
return
}
// create gitea webhook on repo
if err = gitea.RepoCreateWebhook(webhookRepoURL, webhookRepoSecret, usergitea, repo, client); err != nil {
log.Printf("Error creating webhook on gitea repo: %v", err)
if err := gitea.RepoDelete(usergitea, repo, client); err != nil {
log.Printf("Error deleting gitea repo: %v", err)
}
response.execute("Error")
return
}
// send keys to web host via webhook.
if err = gitea.WebhookDeploy(usergitea, repo, keypriv, keypub, webhookURL, webhookSecret); err != nil {
log.Printf("Error passing keys to webserver host: %v", err)
if err = gitea.RepoDelete(usergitea, repo, client); err != nil {
log.Printf("Error deleting gitea repo: %v", err)
}
response.execute("Error")
return
}
log.Printf("Gitea repo deployed and keys sent to webhook listener.")
response.execute("RepoDeployed")
}

@ -5,27 +5,30 @@ import (
"net/http"
"0xacab.org/sindominio/lowry/db"
"0xacab.org/sindominio/lowry/gitea"
"0xacab.org/sindominio/lowry/ldap"
"0xacab.org/sindominio/lowry/mail"
"github.com/gorilla/mux"
)
type server struct {
ldap *ldap.Ldap
mail *mail.Mail
db *db.DB
sess *sessionStore
tmpl *template.Template
ldap *ldap.Ldap
mail *mail.Mail
db *db.DB
sess *sessionStore
tmpl *template.Template
gitea *gitea.Gitea
}
// Serve lowry web site
func Serve(addr string, l *ldap.Ldap, m *mail.Mail, ldb *db.DB) error {
func Serve(addr string, l *ldap.Ldap, m *mail.Mail, ldb *db.DB, g *gitea.Gitea) error {
s := server{
ldap: l,
mail: m,
db: ldb,
sess: initSessionStore(),
tmpl: initTemplate(),
ldap: l,
mail: m,
db: ldb,
sess: initSessionStore(),
tmpl: initTemplate(),
gitea: g,
}
r := mux.NewRouter()
@ -52,6 +55,7 @@ func Serve(addr string, l *ldap.Ldap, m *mail.Mail, ldb *db.DB) error {
r.HandleFunc("/groups/{name}", s.groupHandler)
r.HandleFunc("/groups/{name}/add/", s.addUserGroupHandler).Methods("POST")
r.HandleFunc("/groups/{name}/del/", s.delUserGroupHandler).Methods("POST")
r.HandleFunc("/gitea/", s.giteaHandler)
r.HandleFunc("/bundle.js", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/bundle.js") })
r.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "dist/style.css") })

@ -42,6 +42,7 @@ func initTemplate() *template.Template {
"tmpl/adduser_success.html",
"tmpl/group.html",
"tmpl/groups.html",
"tmpl/gitea.html",
"tmpl/load-password.js",
))
}

@ -0,0 +1,50 @@
{{template "header.html"}}
{{template "header_close.html"}}
{{template "navbar.html" .}}
<div class="container">
<br />
<h1 class="row justify-content-center">Crear repositorio web</h1>
<br />
{{if eq .Data "RepoDeployed"}}
<br />
<br />
<h2 class="row justify-content-center">El repositorio se ha creado en tu cuenta de gitea</h2>
<div class="col-sm-4" style="margin: 0 auto; width: 100%">
<p class="row justify-content-center">Puedes acceder en:&nbsp;<a href="https://git.sindominio.net">https://git.sindominio.net</a></p>
<p class="row" style="text-align: justify;">Sólo tienes que editar un fichero en ese repositorio para activar una web estática en tu carpeta de usuario en /public_html/. Si lo haces, todo el contenido en esa carpeta será sustituido por un blog estático generado con hugo.</p>
</div>
{{else if eq .Data "ErrorGiteaInit"}}
<p class="row justify-content-center">Parece que la funcionalidad no está activada. Contacta con sindomnio.</p>
{{else if eq .Data "Error"}}
<div class="" style="color: #dc3545; text-align: center;">Algo salió mal. Contacta con sindominio.</div>
{{else}}
<br />
<div class="col-sm-4" style="margin: 0 auto; width: 100%">
<p class="row justify-content-center" style="text-align: justify;">Sindominio te ofrece un espacio web para contenidos estáticos. Puedes gestionarlo a través de un repositorio git, editando con nuestra interfaz web gitea.</p>
</div>
<div class="row justify-content-center">
<form class="col-sm-4" id="needs-validation" action="/gitea/" method="post" novalidate>
<div class="form-group">
<label for="reponame">Nombre del repo</label>
<input type="" id="reponame" name="reponame" placeholder="Nombre del repositorio">
</div>
<button id="creategitea" type="submit" class="btn btn-primary justify-content-center" style="width:100%;">Crear</button>
<br />
<div class="form-group">
<p class="row justify-content-center" style="font-size:0.9em; margin-top:10px;"><span style="color:red;"> ¡ATENCIÓN!&nbsp; </span><i>Si el repo ya existe será sobreescrito.</i><p>
</div>
</form>
</div>
{{end}}
<div class="col-sm-4 row justify-content-center" style="margin: 3em auto;">
<p class="row justify-content-center">Más información en:</p>
<ul>
<li class=""><a href="http://sindominio.net/ayudas/gitea-static-webs.html">P&aacute;gina de ayuda Web estáticas</a></li>
<li class=""><a href="mailto:sd@sindominio.net">Lista de correo</a></li>
<li class=""><a href="https://chat.sindominio.net/#/room/#lacolmena:sindominio.net">Chat en matrix</a></li>
</ul>
</div>
</div>
{{template "footer.html"}}

@ -40,11 +40,12 @@
<br />
<div class="row justify-content-center">
<div class="col-md-6">
<p>Bienvenida a lowry, nuestro burocrata preferido. ¿que quieres hacer hoy?</p>
<p>Bienvenida a lowry, nuestro burócrata preferido. ¿qué quieres hacer hoy?</p>
<ul class="list-group">
<li class="list-group-item"><a href="/password/">Cambiar la contraseña</a></li>
{{if eq (printf "%v" .Role) "sindominante"}}
<li class="list-group-item"><a href="/adduser/">Invitar amiga a SinDominio</a></li>
<li class="list-group-item"><a href="/gitea/">Crear repositorio web</a></li>
{{end}}
{{if .IsAdmin}}
<li class="list-group-item"><a href="/users/">Ver listado de cuentas</a></li>

Loading…
Cancel
Save