author | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-02-06 10:59:58 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-02-10 23:18:31 UTC |
parent | 51e7c5c19eea506e5db30b4906b966b9079079b4 |
README.md | +1 | -3 |
chasquid.go | +21 | -0 |
docs/dovecot.md | +56 | -0 |
internal/config/config.go | +2 | -0 |
internal/config/config.pb.go | +39 | -23 |
internal/config/config.proto | +15 | -1 |
test/t-11-dovecot/config/chasquid.conf | +14 | -0 |
test/t-11-dovecot/config/domains/srv/.keep | +0 | -0 |
test/t-11-dovecot/config/dovecot.conf.in | +45 | -0 |
test/t-11-dovecot/config/passwd | +1 | -0 |
test/t-11-dovecot/content | +4 | -0 |
test/t-11-dovecot/hosts | +1 | -0 |
test/t-11-dovecot/msmtprc | +28 | -0 |
test/t-11-dovecot/run.sh | +73 | -0 |
test/util/lib.sh | +4 | -0 |
diff --git a/README.md b/README.md index 70bdcb6..cb1fd50 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,12 @@ It's written in [Go](https://golang.org). * SPF checking. * Monitoring HTTP server, with exported variables and tracing to help debugging. - +* Using dovecot for authentication (experimental). The following are intentionally *not* implemented: * Custom email routing and transport. * DKIM/DMARC checking (although the post-data hook can be used for it). -* Different backends for domain and user configuration (Dovecot authentication - may be implemented in the future). ## Status diff --git a/chasquid.go b/chasquid.go index 9004487..c39e416 100644 --- a/chasquid.go +++ b/chasquid.go @@ -21,6 +21,7 @@ import ( "blitiri.com.ar/go/chasquid/internal/config" "blitiri.com.ar/go/chasquid/internal/courier" + "blitiri.com.ar/go/chasquid/internal/dovecot" "blitiri.com.ar/go/chasquid/internal/maillog" "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/smtpsrv" @@ -95,6 +96,10 @@ func main() { s.SetAliasesConfig(conf.SuffixSeparators, conf.DropCharacters) + if conf.DovecotAuth { + loadDovecot(s, conf.DovecotUserdbPath, conf.DovecotClientPath) + } + // Load certificates from "certs/<directory>/{fullchain,privkey}.pem". // The structure matches letsencrypt's, to make it easier for that case. log.Infof("Loading certificates") @@ -228,6 +233,22 @@ func loadDomain(name, dir string, s *smtpsrv.Server) { } } +func loadDovecot(s *smtpsrv.Server, userdb, client string) { + a := dovecot.Autodetect(userdb, client) + if a == nil { + log.Errorf("Dovecot autodetection failed, no dovecot fallback") + return + } + + if a != nil { + s.SetAuthFallback(a) + log.Infof("Fallback authenticator: %v", a) + if err := a.Check(); err != nil { + log.Errorf("Failed dovecot authenticator check: %v", err) + } + } +} + // Read a directory, which must have at least some entries. func mustReadDir(path string) []os.FileInfo { dirs, err := ioutil.ReadDir(path) diff --git a/docs/dovecot.md b/docs/dovecot.md new file mode 100644 index 0000000..af9577c --- /dev/null +++ b/docs/dovecot.md @@ -0,0 +1,56 @@ + +# Dovecot integration + +As of version 0.04 (2018-02), [chasquid] has _experimental_ integration with +[dovecot] for authenticating users. + +This means that chasquid can ask dovecot to authenticate users, instead/in +addition to having its own per-domain user databases. + +It is experimental because it was added recently, and the semantics and +options are prone to be changed in the future. If you use this feature, please +let the authors know, at chasquid@googlegroups.com. + + +## Configuring dovecot + +The following needs to be added to the Dovecot configuration, usually in +`/etc/dovecot/conf.d/10-master.conf`: + +``` +service auth { + unix_listener auth-chasquid-userdb { + mode = 0660 + user = chasquid + } + unix_listener auth-chasquid-client { + mode = 0660 + user = chasquid + } +} +``` + +If chasquid is running under a different user, adjust the `user = ` lines +accordingly. + +This lets chasquid issue authentication requests to dovecot. + + +## Configuring chasquid + +Add the following line to `/etc/chasquid/chasquid.conf`: + +``` +dovecot_auth: true +``` + +That should be it, because chasquid will "autodetect" the full path to the +dovecot sockets, by looking in the usual places (tested in Debian, Ubuntu, and +CentOS). + +If chasquid can't find them, the paths can be set with the +`dovecot_userdb_path` and `dovecot_client_path` options. + + +[dovecot]: https://dovecot.org +[chasquid]: https://blitiri.com.ar/p/chasquid diff --git a/internal/config/config.go b/internal/config/config.go index 5c70b0c..5bf4abd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -93,4 +93,6 @@ func LogConfig(c *Config) { log.Infof(" Suffix separators: %s", c.SuffixSeparators) log.Infof(" Drop characters: %s", c.DropCharacters) log.Infof(" Mail log: %s", c.MailLogPath) + log.Infof(" Dovecot auth: %v (%q, %q)", + c.DovecotAuth, c.DovecotUserdbPath, c.DovecotClientPath) } diff --git a/internal/config/config.pb.go b/internal/config/config.pb.go index ef42025..6f6b241 100644 --- a/internal/config/config.pb.go +++ b/internal/config/config.pb.go @@ -90,6 +90,18 @@ type Config struct { // If "<syslog>", log using the syslog (at MAIL|INFO priority). // Default: <syslog> MailLogPath string `protobuf:"bytes,12,opt,name=mail_log_path,json=mailLogPath" json:"mail_log_path,omitempty"` + // EXPERIMENTAL - Enable dovecot authentication. + // Domains that don't have an user database will be authenticated via + // dovecot. + DovecotAuth bool `protobuf:"varint,13,opt,name=dovecot_auth,json=dovecotAuth" json:"dovecot_auth,omitempty"` + // EXPERIMENTAL - Dovecot userdb path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-userdb + DovecotUserdbPath string `protobuf:"bytes,14,opt,name=dovecot_userdb_path,json=dovecotUserdbPath" json:"dovecot_userdb_path,omitempty"` + // EXPERIMENTAL - Dovecot client path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-client + DovecotClientPath string `protobuf:"bytes,15,opt,name=dovecot_client_path,json=dovecotClientPath" json:"dovecot_client_path,omitempty"` } func (m *Config) Reset() { *m = Config{} } @@ -104,27 +116,31 @@ func init() { func init() { proto.RegisterFile("config.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 351 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x4d, 0x4f, 0x22, 0x41, - 0x10, 0x86, 0xc3, 0xc2, 0xf2, 0xd1, 0xc0, 0x2e, 0x74, 0x76, 0xb3, 0xbd, 0x7a, 0x41, 0x2e, 0x90, - 0x18, 0xbd, 0x18, 0xe3, 0xc9, 0x03, 0xc2, 0x51, 0xa3, 0x01, 0xef, 0x9d, 0x1a, 0xa6, 0x99, 0xe9, - 0x64, 0xba, 0x7b, 0x52, 0xd5, 0x10, 0xe4, 0x67, 0xf8, 0x8b, 0xcd, 0x34, 0x32, 0x90, 0xe8, 0xb1, - 0x9e, 0xf7, 0x79, 0x53, 0x35, 0x93, 0x66, 0x9d, 0xa5, 0xb3, 0x2b, 0x9d, 0x5c, 0xe7, 0xe8, 0xbc, - 0x1b, 0xbe, 0xd7, 0x58, 0x7d, 0x1a, 0x00, 0x3f, 0x63, 0xcd, 0xd4, 0x91, 0xb7, 0x60, 0x94, 0xa8, - 0x0c, 0x2a, 0xe3, 0xd6, 0xbc, 0x9c, 0xf9, 0x88, 0xf5, 0x0c, 0x6c, 0x65, 0x0c, 0x1e, 0x24, 0xe9, - 0x9d, 0x92, 0x26, 0x12, 0x3f, 0x06, 0x95, 0x71, 0x75, 0xde, 0x35, 0xb0, 0x9d, 0x81, 0x87, 0x85, - 0xde, 0xa9, 0xa7, 0x88, 0x5f, 0xb0, 0x0e, 0x19, 0x9f, 0x4b, 0x88, 0x63, 0x54, 0x44, 0xa2, 0x3a, - 0xa8, 0x8e, 0x5b, 0xf3, 0x76, 0xc1, 0x26, 0x7b, 0xc4, 0xaf, 0x18, 0xa7, 0x75, 0x64, 0x34, 0x91, - 0x76, 0xb6, 0x14, 0x6b, 0x41, 0xec, 0x1f, 0x93, 0x83, 0x7e, 0xcf, 0xce, 0x4f, 0x74, 0xb7, 0x51, - 0x28, 0x7d, 0x46, 0x65, 0xef, 0x67, 0xe8, 0x89, 0xa3, 0xf2, 0xbc, 0x51, 0xf8, 0x9a, 0xd1, 0xc9, - 0x36, 0xe3, 0xac, 0xf6, 0x0e, 0xb5, 0x4d, 0xca, 0x56, 0x3d, 0x7c, 0x5f, 0xff, 0x98, 0x1c, 0xf4, - 0x5b, 0xf6, 0xcf, 0x80, 0xce, 0x64, 0xac, 0x32, 0xbd, 0x51, 0xf8, 0x26, 0x21, 0x51, 0xd6, 0xcb, - 0x48, 0x5b, 0xd1, 0x08, 0x9d, 0x3f, 0x45, 0x3c, 0xfb, 0x4c, 0x27, 0x45, 0xf8, 0xa0, 0x2d, 0xbf, - 0x63, 0xe2, 0xbb, 0x1a, 0x60, 0x42, 0xa2, 0x19, 0x2e, 0xfc, 0xfb, 0xa5, 0x37, 0xc1, 0x84, 0xf8, - 0x7f, 0xd6, 0x0c, 0x3f, 0x35, 0xd6, 0x28, 0x5a, 0x61, 0x41, 0xa3, 0x98, 0x67, 0x1a, 0xf9, 0x25, - 0xeb, 0xd3, 0x7a, 0xb5, 0xd2, 0x5b, 0x49, 0x2a, 0x07, 0x04, 0xef, 0x90, 0x04, 0x0b, 0x4e, 0x6f, - 0x1f, 0x2c, 0x4a, 0xce, 0x47, 0xec, 0x77, 0x8c, 0x2e, 0x97, 0xcb, 0x14, 0x10, 0x96, 0x5e, 0x21, - 0x89, 0x76, 0x50, 0x7f, 0x15, 0x78, 0x5a, 0x52, 0x3e, 0x64, 0xdd, 0x70, 0x69, 0xe6, 0x12, 0x99, - 0x83, 0x4f, 0x45, 0x27, 0x68, 0xed, 0x02, 0x3e, 0xba, 0xe4, 0x05, 0x7c, 0x1a, 0xd5, 0xc3, 0xdb, - 0xb8, 0xf9, 0x08, 0x00, 0x00, 0xff, 0xff, 0xfb, 0x6f, 0x6c, 0xff, 0x2b, 0x02, 0x00, 0x00, + // 409 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x41, 0x8f, 0x12, 0x31, + 0x14, 0xc7, 0x83, 0xb8, 0x2c, 0x14, 0xd8, 0x5d, 0xaa, 0xc6, 0xaa, 0x17, 0xdc, 0xcb, 0x92, 0x18, + 0xf7, 0x62, 0x8c, 0x27, 0x0f, 0x08, 0x47, 0x8d, 0x86, 0xd5, 0x73, 0xf3, 0x66, 0xa6, 0xcc, 0x34, + 0x99, 0x69, 0x27, 0xef, 0x75, 0x08, 0xf2, 0x3d, 0xfc, 0xbe, 0xa6, 0x0f, 0x18, 0x30, 0xee, 0xb1, + 0xff, 0xdf, 0xef, 0xdf, 0x76, 0xde, 0x54, 0x8c, 0x52, 0xef, 0xd6, 0x36, 0xbf, 0xaf, 0xd1, 0x07, + 0x7f, 0xfb, 0xe7, 0x42, 0xf4, 0x16, 0x1c, 0xc8, 0xd7, 0xa2, 0x5f, 0x78, 0x0a, 0x0e, 0x2a, 0xa3, + 0x3a, 0xd3, 0xce, 0x6c, 0xb0, 0x6a, 0xd7, 0xf2, 0x4e, 0xdc, 0x54, 0xb0, 0xd5, 0x19, 0x04, 0xd0, + 0x64, 0x77, 0x46, 0x57, 0x89, 0x7a, 0x32, 0xed, 0xcc, 0xba, 0xab, 0x71, 0x05, 0xdb, 0x25, 0x04, + 0x78, 0xb0, 0x3b, 0xf3, 0x2d, 0x91, 0x6f, 0xc5, 0x88, 0xaa, 0x50, 0x6b, 0xc8, 0x32, 0x34, 0x44, + 0xaa, 0x3b, 0xed, 0xce, 0x06, 0xab, 0x61, 0xcc, 0xe6, 0xfb, 0x48, 0xbe, 0x17, 0x92, 0x9a, 0xa4, + 0xb2, 0x44, 0xd6, 0xbb, 0x56, 0x7c, 0xca, 0xe2, 0xe4, 0x44, 0x8e, 0xfa, 0x67, 0xf1, 0xe6, 0x4c, + 0xf7, 0x1b, 0x83, 0x3a, 0x94, 0xd4, 0xf6, 0x2e, 0xb8, 0xa7, 0x4e, 0xca, 0xf7, 0x8d, 0xc1, 0x9f, + 0x25, 0x9d, 0x9d, 0x56, 0x79, 0x67, 0x83, 0x47, 0xeb, 0xf2, 0xb6, 0xd5, 0xe3, 0xef, 0x9b, 0x9c, + 0xc8, 0x51, 0xff, 0x28, 0x5e, 0x56, 0x60, 0x4b, 0x9d, 0x99, 0xd2, 0x6e, 0x0c, 0xfe, 0xd6, 0x90, + 0x1b, 0x17, 0x74, 0x62, 0x9d, 0xba, 0xe4, 0xce, 0xf3, 0x88, 0x97, 0x07, 0x3a, 0x8f, 0xf0, 0x8b, + 0x75, 0xf2, 0x93, 0x50, 0x8f, 0xd5, 0x00, 0x73, 0x52, 0x7d, 0xbe, 0xe1, 0x8b, 0xff, 0x7a, 0x73, + 0xcc, 0x49, 0xbe, 0x12, 0x7d, 0x1e, 0x6a, 0x66, 0x51, 0x0d, 0xf8, 0x80, 0xcb, 0xb8, 0x5e, 0x5a, + 0x94, 0xef, 0xc4, 0x84, 0x9a, 0xf5, 0xda, 0x6e, 0x35, 0x99, 0x1a, 0x10, 0x82, 0x47, 0x52, 0x82, + 0x9d, 0x9b, 0x3d, 0x78, 0x68, 0x73, 0x79, 0x27, 0xae, 0x33, 0xf4, 0xb5, 0x4e, 0x0b, 0x40, 0x48, + 0x83, 0x41, 0x52, 0x43, 0x56, 0xaf, 0x62, 0xbc, 0x68, 0x53, 0x79, 0x2b, 0xc6, 0x7c, 0xd3, 0xd2, + 0xe7, 0xba, 0x86, 0x50, 0xa8, 0x11, 0x6b, 0xc3, 0x18, 0x7e, 0xf5, 0xf9, 0x0f, 0x08, 0x45, 0xfc, + 0x89, 0x99, 0xdf, 0x98, 0xd4, 0x07, 0x0d, 0x4d, 0x28, 0xd4, 0x78, 0xda, 0x99, 0xf5, 0x57, 0xc3, + 0x43, 0x36, 0x6f, 0x42, 0x21, 0xef, 0xc5, 0xb3, 0xa3, 0xd2, 0x90, 0xc1, 0x2c, 0xd9, 0x6f, 0x76, + 0xb5, 0x9f, 0xeb, 0x01, 0xfd, 0x62, 0xc2, 0x5b, 0x9e, 0xf9, 0x69, 0x69, 0xe3, 0x6c, 0xd8, 0xbf, + 0xfe, 0xc7, 0x5f, 0x30, 0x89, 0x7e, 0xd2, 0xe3, 0xe7, 0xf9, 0xe1, 0x6f, 0x00, 0x00, 0x00, 0xff, + 0xff, 0x37, 0xa3, 0x19, 0x18, 0xae, 0x02, 0x00, 0x00, } diff --git a/internal/config/config.proto b/internal/config/config.proto index 298e496..e23427a 100644 --- a/internal/config/config.proto +++ b/internal/config/config.proto @@ -74,5 +74,19 @@ message Config { // If "<syslog>", log using the syslog (at MAIL|INFO priority). // Default: <syslog> string mail_log_path = 12; -} + // EXPERIMENTAL - Enable dovecot authentication. + // Domains that don't have an user database will be authenticated via + // dovecot. + bool dovecot_auth = 13; + + // EXPERIMENTAL - Dovecot userdb path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-userdb + string dovecot_userdb_path = 14; + + // EXPERIMENTAL - Dovecot client path. If dovecot_auth is set and this + // is not, we will try to autodetect it. + // Example: /var/run/dovecot/auth-client + string dovecot_client_path = 15; +} diff --git a/test/t-11-dovecot/config/chasquid.conf b/test/t-11-dovecot/config/chasquid.conf new file mode 100644 index 0000000..e5c50a4 --- /dev/null +++ b/test/t-11-dovecot/config/chasquid.conf @@ -0,0 +1,14 @@ +smtp_address: ":1025" +submission_address: ":1587" +submission_over_tls_address: ":1465" +monitoring_address: ":1099" + +mail_delivery_agent_bin: "test-mda" +mail_delivery_agent_args: "%to%" + +data_dir: "../.data" +mail_log_path: "../.logs/mail_log" + +dovecot_auth: true +dovecot_userdb_path: "/tmp/chasquid-dovecot-test/run/auth-userdb" +dovecot_client_path: "/tmp/chasquid-dovecot-test/run/auth-client" diff --git a/test/t-11-dovecot/config/domains/srv/.keep b/test/t-11-dovecot/config/domains/srv/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/t-11-dovecot/config/dovecot.conf.in b/test/t-11-dovecot/config/dovecot.conf.in new file mode 100644 index 0000000..d770287 --- /dev/null +++ b/test/t-11-dovecot/config/dovecot.conf.in @@ -0,0 +1,45 @@ +base_dir = $ROOT/run/ +log_path = $ROOT/dovecot.log +ssl = no + +default_internal_user = $USER +default_login_user = $USER + + +passdb { + driver = passwd-file + args = $ROOT/passwd +} + +userdb { + driver = passwd-file + args = $ROOT/passwd +} + +service auth { + unix_listener auth { + mode = 0666 + } +} + +# Dovecot refuses to start without protocols, so we need to give it one. +protocols = imap + +service imap-login { + chroot = + inet_listener imap { + address = 127.0.0.1 + port = 0 + } +} + +service anvil { + chroot = +} + +# Turn on debugging information, to help troubleshooting issues. +auth_verbose = yes +auth_debug = yes +auth_debug_passwords = yes +auth_verbose_passwords = yes +mail_debug = yes diff --git a/test/t-11-dovecot/config/passwd b/test/t-11-dovecot/config/passwd new file mode 100644 index 0000000..0182c1a --- /dev/null +++ b/test/t-11-dovecot/config/passwd @@ -0,0 +1 @@ +user@srv:{plain}password:1000:1000::/home/user diff --git a/test/t-11-dovecot/content b/test/t-11-dovecot/content new file mode 100644 index 0000000..76a8b16 --- /dev/null +++ b/test/t-11-dovecot/content @@ -0,0 +1,4 @@ +Subject: Prueba desde el test + +Crece desde el test el futuro +Crece desde el test diff --git a/test/t-11-dovecot/hosts b/test/t-11-dovecot/hosts new file mode 100644 index 0000000..3ab990d --- /dev/null +++ b/test/t-11-dovecot/hosts @@ -0,0 +1 @@ +srv localhost diff --git a/test/t-11-dovecot/msmtprc b/test/t-11-dovecot/msmtprc new file mode 100644 index 0000000..99c540d --- /dev/null +++ b/test/t-11-dovecot/msmtprc @@ -0,0 +1,28 @@ +account default + +host srv +port 1587 + +tls on +tls_trust_file config/certs/srv/fullchain.pem + +from user@srv + +auth on +user user@srv +password password + +account smtpport : default +port 1025 + +account subm_tls : default +port 1465 +tls_starttls off + +account baduser : default +user unknownuser@srv +password secretpassword + +account badpasswd : default +user user@srv +password badsecretpassword diff --git a/test/t-11-dovecot/run.sh b/test/t-11-dovecot/run.sh new file mode 100755 index 0000000..6403056 --- /dev/null +++ b/test/t-11-dovecot/run.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# This test checks that we can use dovecot as an authentication mechanism. +# +# Setup: +# - chasquid listening on :1025. +# - dovecot listening on unix sockets in .dovecot/ + +set -e +. $(dirname ${0})/../util/lib.sh + +init + +if ! dovecot --version > /dev/null; then + skip "dovecot not installed" + exit 0 +fi + +# Create a temporary directory for dovecot to use, and generate the dovecot +# config based on the template. +# Note the lenght of the path must be < 100, because unix sockets have a low +# limitation, so we use a directory in /tmp, which is not ideal, as a +# workaround. +export ROOT="/tmp/chasquid-dovecot-test" +mkdir -p $ROOT $ROOT/run +rm -f $ROOT/dovecot.log + +envsubst < config/dovecot.conf.in > $ROOT/dovecot.conf +cp -f config/passwd $ROOT/passwd + +dovecot -F -c $ROOT/dovecot.conf & + +# Early tests: run dovecot-auth-cli for testing purposes. These fail early if +# there are obvious problems. +OUT=$(dovecot-auth-cli $ROOT/run/auth exists user@srv || true) +if [ "$OUT" != "yes" ]; then + fail "user does not exist: $OUT" +fi + +OUT=$(dovecot-auth-cli $ROOT/run/auth auth user@srv password || true) +if [ "$OUT" != "yes" ]; then + fail "auth failed: $OUT" +fi + + +# Set up chasquid, using dovecot as authentication backend. +generate_certs_for srv + +mkdir -p .logs +chasquid -v=2 --logfile=.logs/chasquid.log --config_dir=config & +wait_until_ready 1025 + +# Send an email as user@srv successfully. +run_msmtp user@srv < content +wait_for_file .mail/user@srv +mail_diff content .mail/user@srv + +# Fail to send to nobody@srv (user does not exist). +if run_msmtp nobody@srv < content 2> /dev/null; then + fail "successfuly sent an email to a non-existent user" +fi + +# Fail to send from baduser@srv (user does not exist). +if run_msmtp -a baduser user@srv < content 2> /dev/null; then + fail "successfully sent an email with a bad user" +fi + +# Fail to send with an incorrect password. +if run_msmtp -a badpasswd user@srv < content 2> /dev/null; then + fail "successfully sent an email with a bad password" +fi + +success diff --git a/test/util/lib.sh b/test/util/lib.sh index 145f02d..aed646d 100644 --- a/test/util/lib.sh +++ b/test/util/lib.sh @@ -49,6 +49,10 @@ function add_user() { >> .add_user_logs } +function dovecot-auth-cli() { + go run ${TBASE}/../../cmd/dovecot-auth-cli/dovecot-auth-cli.go "$@" +} + function run_msmtp() { # msmtp will check that the rc file is only user readable. chmod 600 msmtprc